GodUIGodUI
129Follow on X

Anatomy of the Floating Toolbar

How one AnimatePresence and a shared spring turn a flat actions array into a toolbar that springs into place and lifts under the pointer.

01/04Anatomy
shell
backdrop-blur pill, role=toolbar
action
an idle motion.button
active
aria-pressed, filled fill
tsx
<motion.div role="toolbar" aria-label="Floating toolbar" layout>{actions.map((action) => (  <motion.button    key={action.label}    layout    aria-pressed={action.active}    className={      action.active        ? "bg-primary text-primary-foreground"        : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"    }  >    {action.icon}  </motion.button>))}</motion.div>
01

Anatomy

Floating Toolbar looks like a simple contextual bar, but it's built from one spring config reused three times: once for the whole shell mounting in, once for the shell leaving, and once per action button, per interaction. The component itself is small; the feel comes from reusing that one number set everywhere.

actions is a flat array, the same approach as Context Menu: plain objects the toolbar maps over instead of compound components. Structurally, an "active" action is identical to an idle one; only aria-pressed and two class names differ.

Every button, and the shell around them, carries layout. Add or remove an action and Framer measures the before/after boxes and animates the diff, so the row never jump-cuts when its contents change.

02/04The motion

{ type: "spring", stiffness: 520, damping: 32 }

enter
opacity 0, y 12, scale 0.92 → spring 520/32
exit
opacity 0, y 8, scale 0.95: a plain tween
reduced motion
collapses to an opacity-only fade
tsx
<AnimatePresence>{open && (  <motion.div    initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 12, scale: 0.92 }}    animate={{ opacity: 1, y: 0, scale: 1 }}    exit={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 8, scale: 0.95 }}    transition={{ type: "spring", stiffness: 520, damping: 32 }}  >    {/* actions */}  </motion.div>)}</AnimatePresence>
02

The motion

AnimatePresence mounts and unmounts the whole motion.div, and the enter springs opacity, y and scale on a stiff, barely-overshooting spring.

520/32 is a much stiffer, more critically damped pair than the tooltip's 170/12/0.1. The toolbar should feel immediate, as if it was always there and just became visible. The exit target doesn't mirror the enter (y: 8, not 12; scale: 0.95, not 0.92): it travels a shorter distance on the way out, because a lingering overshoot on dismissal would read as sluggish.

03/04Per-action spring

whileHover={ y: -2, scale: 1.08 } · whileTap={ scale: 0.94 }

idle
resting scale, no transform
hover
y: -2, scale: 1.08
press
whileTap scale: 0.94
tsx
<motion.buttonlayoutwhileHover={reduceMotion ? undefined : { y: -2, scale: 1.08 }}whileTap={reduceMotion ? undefined : { scale: 0.94 }}transition={{ type: "spring", stiffness: 520, damping: 32 }}>{action.icon}</motion.button>
03

Per-action spring

The same 520/32 spring drives every action's hover and press state, independent of the shell.

whileHover/whileTap are declarative targets rather than event handlers you wire up yourself. Framer owns the pointer listeners and animates toward whichever state is currently true. Because it's the same spring as the shell's entrance, a button lifting under the cursor moves like part of the toolbar it floats in instead of a separate hover effect layered on top.

04/04Result
04

The result

Two springs, one number set: stiffness: 520, damping: 32 moves the whole shell in and out, and lifts and settles each button under the pointer.

Accessibility

role="toolbar" and aria-label="Floating toolbar" are hard-coded onto the shell, and every action gets aria-label={action.label} plus aria-pressed={action.active}, so a screen reader announces exactly which tool is active without reading any visual state. useReducedMotion strips every transform from both the shell and its actions, leaving nothing but the opacity fades: the toolbar still appears and disappears, and buttons still respond to hover/tap, just without the y-shift, scale, or overshoot.

Motion Score

Floating ToolbarCC: Paint-triggering
SopacityFade / cross-fade
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →