Dustin Johnson ← Design Primitives
Design Primitive Reference · Part III · April 2026

Hover & Motion

How buttons should change when a cursor enters them — and the system-level rules that keep those changes coherent across a full button hierarchy.

1. The Cursor

Before anything animates, the cursor itself is the first signal. It changes the moment the pointer enters the element, before any transition begins — instant, binary, unmissable.

The debate over cursor: pointer on buttons is surprisingly unresolved. The HTML spec associates the pointer (hand) cursor with links, not buttons — the browser default for <button> elements is cursor: default (the arrow). The argument for keeping the arrow: it correctly distinguishes buttons (which perform actions) from links (which navigate). The argument for the pointer: users have been trained by two decades of web use to associate the hand cursor with "this is clickable," and withholding it on buttons creates a moment of uncertainty.

Hover over each button to see the cursor change
cursor: default
cursor: pointer

In practice, the vast majority of modern design systems — shadcn/ui, Material, Carbon, Apple HIG web components — use cursor: pointer on all interactive elements including buttons. The semantic purity argument has lost to user expectation. The recommendation: use pointer. It removes one moment of doubt, and the semantic distinction between links and buttons is better communicated through visual design than through cursor shape.

2. Transition Timing

Every hover effect should be animated, not instantaneous. The transition between default and hover states is what makes an interface feel crafted rather than mechanical. But the parameters matter.

ParameterValueRationale
Enter duration120–200msFast enough to feel immediate, slow enough to register as a transition. The sweet spot for color/opacity shifts is 150ms.
Leave duration150–250msSlightly longer than enter. The asymmetry feels natural — response is snappy, return to rest is relaxed.
Enter easingease-outDecelerating curve. The change happens quickly at first and settles gently — feels responsive.
Active (pressed)40–80msMust feel instant. The pressed state should snap into place. A sluggish press transition is one of the most common sources of perceived UI lag.
/* A well-tuned transition stack */ .btn { transition: background-color 0.15s ease-out, border-color 0.15s ease-out, color 0.15s ease-out, box-shadow 0.2s ease-out, transform 0.2s ease-out; } .btn:active { transition-duration: 0.06s; }
The properties you transition matter for performance. Opacity and transform are composited by the GPU and never trigger layout recalculation. Background-color and box-shadow trigger repaint but not reflow — acceptable for buttons. Avoid transitioning width, height, padding, or border-width — these trigger layout and can cause visible jank in dense UIs.

3. Hover Strategies by Fill Type

The right hover effect depends on the button's resting fill state. A solid button can't get darker if it's already black. An outlined button that fills to match the solid button's color collapses the visual hierarchy. Each fill type has its own set of viable strategies.

Solid (Primary) Buttons

Hover over each button to see the effect
opacity → 82%
Lighter bg
translateY + shadow
scale(1.03)

Opacity reduction is the simplest and most universal approach. It works on any color, including pure black. The button becomes slightly translucent, letting the background show through. Keep it subtle — 82–90% opacity is the safe range. Below ~75%, text contrast can fail.

Lightening the background is the most common approach when the primary isn't pure black. Shadcn/ui uses bg-primary/90. This works well for hues or dark grays, but not for #000 — "lightening" pure black to a visible gray feels washed out rather than interactive.

Lift (translateY + shadow increase) communicates hover through spatial change rather than tonal change, which means it works universally regardless of fill color. The subtle upward shift (1–2px) creates a physical metaphor: "this element is rising to meet your cursor." This is the most premium-feeling option — used by Apple, Stripe, and Vercel for primary actions.

Scale is energetic and attention-grabbing — appropriate for marketing CTAs but potentially distracting in dense application UIs where many buttons cluster together.

Outlined (Secondary) Buttons

Hover over each button to see the effect
Outlined → solid
Subtle bg fill
Border darkens

Fill in (outlined → solid) is the most dramatic hover effect. But it has a systemic problem: if the outlined button fills to the same color as the primary button next to it, the two become visually identical on hover. Use fill-in only when the outlined button won't appear adjacent to a solid, or when the fill color is a lighter shade rather than the same dark.

Tint (adding a 5–8% background) is the safest default. The button stays clearly "secondary" even on hover. This is the approach used by most modern systems.

Border intensification — darkening a faint border on hover — is a good complement to tint. If the resting border is at 20% opacity, increasing it to 40–50% on hover makes the button feel more defined.

Tinted & Ghost Buttons

Hover over each button to see the effect
6% → 12% fill
Same fill-in problem
0% → 5% fill

Ghost → tint is the most universally safe hover effect. The ghost button goes from invisible-background to barely-visible-background. It can't collide with any other button's hover state. This is why every modern design system uses it.

4. The Active (Pressed) State

The :active state is triggered while the user is physically pressing the button. It should feel like pressing a physical surface: immediate, slight, and brief.

Click and hold the button to see the active state
Hover lifts · Active depresses
Hover tints · Active deepens

The best active states reverse the hover direction. If hover lifts up (translateY(-1px)), active pushes back down (translateY(0)). If hover lightens, active darkens. This creates a satisfying push-release cycle that mirrors physical button mechanics. The active transition should be 40–80ms — very fast, to feel "snappy."

For tinted and ghost buttons, the active state typically deepens the tint further: if hover is 8% opacity, active might be 14–18%. The key constraint: the active state of a secondary button should never approach the visual weight of the primary button's resting state.

5. Icon Behavior on Hover

Icons within buttons can stay static (most common), animate (rotate, shift, bounce), or appear/disappear on hover. Each has appropriate uses.

Hover to see the trailing arrow appear
Icon reveals on hover

Revealing an icon on hover — typically a trailing arrow — is an elegant pattern for "continue" or "learn more" buttons. It communicates directionality and forward momentum. This pattern is used by Stripe, Linear, and Vercel on marketing pages.

Animating an existing icon — nudging a trailing arrow rightward by 3–4px on hover — is a lighter version. It works well for navigation buttons and link-style buttons.

Keeping icons static is the right choice for application buttons in dense UIs. The button's tonal hover effect is sufficient. Icon animation in toolbars and forms creates visual chaos.

Animate icons on hover only for buttons that appear in isolation — hero CTAs, card actions, standalone links. In toolbars, forms, or button groups where multiple buttons cluster together, let the tonal change carry the hover signal alone.

6. The System Constraint

A hover strategy only works as a system when every button's hover state preserves the hierarchy that the resting states established. If hover collapses the visual distinction between primary and secondary, the system is broken — even if each button's hover effect looks good in isolation.

The Cardinal Rules

Rule 1: A secondary button's hover state must never match the primary button's resting state. This is the most common violation. If your primary is solid black and your outlined secondary fills to solid black on hover, the user's brain can't distinguish between "I'm hovering on the secondary" and "that's the primary." The fix: make the outlined hover a tint or a lighter fill, not a full solid.

Rule 2: Hover should move in one direction on the contrast scale. Solid buttons get lighter (or lift). Outlined buttons get slightly filled. Tinted buttons get deeper. Ghost buttons get tinted. Every tier moves toward its next-higher neighbor, but never reaches it.

Rule 3: The same hover technique should apply to the same fill type everywhere. If your outlined buttons tint on hover in the header, they should tint on hover in the sidebar, in modals, and in forms. Inconsistency in hover behavior is more disorienting than inconsistency in resting state, because hover is a real-time response to user action.

Rule 4: Hover contrast must still pass accessibility. If your solid button's text contrast is 13:1 at rest, reducing opacity to 82% on hover drops it to ~10:1 — still passing. But if your outlined button's border is already marginal at 2.5:1, hover should improve it, not leave it unchanged.

7. Three Complete Systems

Below are three internally consistent hover systems applied to a full four-tier button hierarchy. Each uses a different primary-button hover strategy and derives all other tiers from it.

System A: The Overlay

Primary lightens via white overlay. All other tiers deepen their fill. No layout shift. Purely tonal. The safest, most conservative option — used by shadcn/ui, Radix, Linear, and most SaaS products.

Hover each button

System B: The Lift

Primary lifts and gains shadow — spatial movement reserved for the most important action. Secondary border intensifies with a tint. Common on marketing pages and in design-forward products like Stripe and Vercel.

Hover each button · Click primary for active state

System C: The Fill-Up

Primary fades slightly. Secondary fills in fully — text inverts. The boldest system. It works when the secondary is rarely adjacent to the primary, or when the fill-in uses a lighter shade. Use with caution in dense UIs.

Hover each button

8. The Universal Overlay Technique

The most reusable hover technique across a button system is the semi-transparent overlay: a white or black layer at low opacity, applied via a pseudo-element, that works regardless of fill color. White overlay to lighten dark fills; black overlay to darken light fills.

/* One pattern for all buttons */ .btn { position: relative; overflow: hidden; z-index: 0; } .btn::after { content: ''; position: absolute; inset: 0; z-index: -1; opacity: 0; transition: opacity 0.15s ease-out; } /* Dark fills: lighten */ .btn-solid::after { background: white; } .btn-solid:hover::after { opacity: 0.12; } /* Light fills: darken */ .btn-outlined::after, .btn-ghost::after { background: black; } .btn-outlined:hover::after, .btn-ghost:hover::after { opacity: 0.05; }

This technique is powerful because the same overlay code works on any base color. Change the brand color and every hover state updates automatically — no manual retuning. The simpler background: primary/90 approach achieves the same for solid buttons without the pseudo-element, but doesn't generalize as cleanly to outlined and ghost variants.

9. A Note on Touch

Everything above is desktop-only. On touch devices, there is no hover. Key considerations for desktop designers to keep in mind now, with full mobile treatment deferred to a future part:

Never gate information behind hover. If an icon only appears on hover, mobile users will never see it. Use hover as enhancement, not the sole mechanism for communicating something.

The :active state does double duty on mobile. Since there's no hover, active is the only visual feedback a touch user gets. It needs to be slightly more pronounced than on desktop.

Use @media (hover: hover) to scope hover styles. This media query applies hover effects only on devices with true pointer hover, preventing the "sticky hover" problem on touch devices.

@media (hover: hover) { .btn:hover::after { opacity: 0.12; } } /* Active state applies everywhere */ .btn:active { transform: scale(0.97); transition-duration: 0.06s; }

Part III of the Button Taxonomy. Part I covers structure. Part II covers contrast and construction.
The best hover state is the one that preserves the hierarchy.