GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 CSS Button Click Feedback Effects, Copy-Paste

CSS button click feedback is the flash, scale, or color shift a button shows the instant it's pressed, confirming a tap landed — nine effects below, one

Auto-plays · click a tile to jump to its section · all nine in one zip

These nine css button design snippets sit in the order one tap actually moves through a screen: 01 and 02 confirm a plain button press, 03 and 04 change how or when that confirmation fires, 05 and 06 scale the same idea up to a whole row or card, and 07 through 09 close on an icon jitter, a checkbox, and an inline link. The grid above replays each state on a loop by itself — every panel sits in its own iframe with nothing wired up to click, so watching is the only option here. Four of the nine, 02, 03, 04, and 06, reach for touch-specific media queries or touch-action instead of treating every tap as a mouse click wearing a costume. If item 07's quick like-confirm is the piece you actually want to grow, nine CSS heart animations spend a whole set on that one gesture, and if 01's ring should keep travelling around the button edge instead of pulsing once, the CSS border animations pick that thread up.

01Ring pulse (outline, not fill)

Why a css button animation on click outlives the press

A box-shadow ring blooms outward from the button's own edge and fades, triggered by a pointerdown listener that swaps in an .is-ringing class rather than leaning on :active alone, and this update doubles the ring into two coats that spread at different speeds and decay in steps, so the wavefront reads as irregular rather than perfectly smooth. So the ring finishes its full animation even after the finger or pointer lifts.

Ring pulse (outline, not fill)
Open in a new tab

01 · Ring pulse (outline, not fill) — Measured area 4.103% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

box-shadowpointerdown0.6초

Where a ring beats a ripple

Reach for it on icon buttons that already use a filled ripple elsewhere, where a ring supplies a second, visibly different signal instead of stacking two near-identical waves.

.btn::after {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  box-shadow: 0 0 0 0 rgba($color, .55);
  opacity: 0;
  pointer-events: none;
}

.btn.is-ringing::after {
  animation: ring $duration $easing forwards;
}

@keyframes ring {
  0%   { box-shadow: 0 0 0 0 rgba($color, .55); opacity: 1; }
  100% { box-shadow: 0 0 0 26px rgba($color, 0); opacity: 0; }
}

02Press shrink + shadow flatten

What shrinks, and by how much

While pressed, the button shrinks to scale(.96) and its box-shadow flattens at the same time, then springs back the instant the pointer lifts with no spring bounce, and on the press itself two ring coats now spread outward at different speeds and damp away, so the press reads as travelling beyond the button's edge.

Press shrink + shadow flatten
Open in a new tab

02 · Press shrink + shadow flatten — Measured area 6.326% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

scalebox-shadow0.2초

Why submit buttons lean on it

touch-action: manipulation strips the tap delay and double-tap zoom that would otherwise blur that timing on a phone, which is why submit and checkout buttons — anywhere confirming the press matters most — lean on it.

.btn {
  transform: scale(1);
  transition: transform $duration $easing, box-shadow $duration $easing;
  touch-action: manipulation;
}

.btn:active {
  transform: scale(.96);
  box-shadow: $shadow-press;
}

03Touch-only :active via pointer: coarse

Scoping :active to touch only

@media (hover: none) and (pointer: coarse) scopes the :active background to touch devices only, so a mouse-driven browser never even parses that rule and can't leave a stuck css button hover gray behind after a click, and on touch the press now also wakes two ring coats that spread irregularly and damp away.

Touch-only :active via pointer: coarse
Open in a new tab

03 · Touch-only :active via pointer: coarse — Measured area 8.344% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

@media (pointer: coarse)hover: none:active

Where the stuck gray disappears

It fits mobile web buttons that otherwise leave a gray background lingering after every tap.

@media (hover: none) and (pointer: coarse) {
  .btn:active {
    background: $stage-ink;
    color: $subject-cream;
  }
}

04Long-press delay reveal (no fill bar)

Holding the response back 600ms

Nothing happens the instant a finger lands — a transition-delay of 600ms holds the response back, and only once that threshold passes does the card grow while its border and shadow darken, with two ring coats spreading from the touch point at different speeds and damping out.

Long-press delay reveal (no fill bar)
Open in a new tab

04 · Long-press delay reveal (no fill bar) — Measured area 10.674% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

:active + transition 지연touch-action: none0.6초

Why touch-action: none matters

touch-action: none keeps the browser's own scroll gesture from competing with the hold, which suits a card previewing a context menu, where a short accidental tap should trigger nothing at all.

.card {
  transition: transform $duration $easing $hold,
              box-shadow $duration $easing $hold,
              border-color $duration $easing $hold;
  touch-action: none;
}

.card:active {
  transform: scale(1.06);
  box-shadow: $shadow-raised;
  border-color: $color;
}

05List row tap highlight

A flash instead of the gray rectangle

With -webkit-tap-highlight-color turned off, tapping a row flashes a custom background color that fades back out, instead of leaving the browser's own gray tap rectangle behind, and a two-coat ripple now travels along the row's inner edge and damps away, adding one more grain to the touch. Each row also holds a 44px minimum height so the tappable strip matches a finger's width rather than the text inside it.

List row tap highlight
Open in a new tab

05 · List row tap highlight — Measured area 19.752% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

-webkit-tap-highlight-color:active0.2초

Sizing the row to a finger

That matters most on settings menus and chat lists where rows sit close together.

.list li {
  min-height: 44px;
  -webkit-tap-highlight-color: transparent;
  transition: background $duration $easing;
}

.list li:active {
  background: rgba($color, .18);
  transition: background 0s;
}

06Card tap scale + brightness dim

Scale and brightness together

scale(.98) and filter: brightness(.94) apply together the moment a card is tapped, so the whole surface reads as pressed rather than just its edges, while two ring coats spread at different depths along the inner border and damp out fast, rippling inside the card without ever crossing its bounds.

Card tap scale + brightness dim
Open in a new tab

06 · Card tap scale + brightness dim — Measured area 7.167% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

scalefilter: brightnesstouch-action

Blocking the double-tap zoom

touch-action: manipulation blocks the double-tap zoom a quick second tap would otherwise trigger, which fits product or thumbnail cards where the tappable surface is large.

.card {
  transform: scale(1);
  filter: brightness(1);
  transition: transform $duration $easing, filter $duration $easing;
  touch-action: manipulation;
}

.card:active {
  transform: scale(.98);
  filter: brightness(.94);
}

07Tap-confirm jitter (not the error shake)

A 140ms jitter, not a shake

Right after a tap, the icon jitters twice within about 140ms at a ±2px amplitude while its color fills from a pale tint to solid, mimicking a phone's haptic buzz on screen.

Tap-confirm jitter (not the error shake)
Open in a new tab

07 · Tap-confirm jitter (not the error shake) — Measured area 1.997% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

translate 지터:active0.14초

Where a small motion fits

That's a far smaller motion, and a different purpose, than a three-swing error shake — reach for it on like or favorite buttons that need a brief success confirmation rather than a warning.

.btn:active { animation: jitter $duration $easing; }
.btn:active .btn__icon { color: $error; }

@keyframes jitter {
  0%   { transform: scale(1)    translate(0, 0); }
  25%  { transform: scale(1.18) translate(-2px, 1px); }
  50%  { transform: scale(1.1)  translate(2px, -1px); }
  75%  { transform: scale(1.16) translate(-1px, 1px); }
  100% { transform: scale(1)    translate(0, 0); }
}

08Checkbox tap feedback

Drawing the check mark

Tapping a custom checkbox draws its check mark through stroke-dashoffset while the box itself pops to scale(1.08) once, so the state change gets confirmed twice in the same moment.

Checkbox tap feedback
Open in a new tab

08 · Checkbox tap feedback — Measured area 1.833% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

stroke-dashoffsetscale0.2초

A 26px box in a 44px target

The real input stays visually hidden but keeps its own focus ring, and the label wrapping both spans holds a 44px tap target even though the visible box itself measures only 26px.

.cb__input:checked ~ .cb__box {
  border-color: $color;
  background: $color;
  transform: scale(1.08);
}

.cb__input:checked ~ .cb__box .cb__check {
  stroke-dashoffset: 0;
}

.cb__check {
  stroke-dasharray: 24;
  stroke-dashoffset: 24;
  transition: stroke-dashoffset $duration $easing;
}

09Inline link tap feedback

A flash behind the letters

Tapping a link inside a paragraph flashes a short color block behind the letters and lets it fade back out, leaving a press signal in place even where the browser's own tap-highlight has been switched off. Padding paired with a matching negative margin keeps the flash aligned to the text instead of spilling past the link's own box.

Inline link tap feedback
Open in a new tab

09 · Inline link tap feedback — Measured area 1.469% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

background:active0.14초

Keeping the block on the text

That suits blog posts and terms pages where links sit inline inside running sentences.

.p__link {
  padding: 2px 3px;
  margin: -2px -3px;
  background: transparent;
  transition: background $duration $easing;
}

.p__link:active {
  background: rgba($color, .32);
  transition: background 0s;
}

Where it breaks — the timing trap

An effect that moves nothing but a background alpha can look alive and still measure as still — the same wall the glowing panels in the AI landing page orb sections ran into during their own render pass.

A linear fade the render check nearly missed

Item 09 is the one demo on this page that almost failed the render check every collection here has to pass before it goes live. Its flash is a single linear background-color fade, and a linear curve spreads a color change into equal steps across every frame rather than clustering it anywhere — unlike an eased curve, whose steps bunch up hardest right where two frames sit closest together. This site's render pipeline marks a frame as unmoved once its per-pixel color delta falls under a fixed threshold, and spreading 09's fade evenly across 24 frames left several of them sitting right on that line, nearly reading as a static block of text with a link glued to it. The version that finally passed measures an average intensity of 16.2 against a floor of 15, and 8 out of 24 frames counted as moved against a floor of 6 — a real margin now, but one timing tweak away from slipping back under. MDN's animation-timing-function reference covers exactly this gap between linear and eased pacing. The narrowed timing that fixed it ships in the archive, unlocked with e8dzq4mp typed exactly as it reads on this line, no spaces added anywhere.

Accessibility

What survives reduced motion

All nine drop their motion under prefers-reduced-motion: reduce, but seven of them keep the confirmation itself rather than just switching a transition off. Everything except 01 and 07 sets transition: none, so whatever end state a tap already produces — a smaller scale, a darker card, a highlighted row, a drawn checkmark, a flashed link — appears instantly instead of easing in. 07 removes its jitter animation outright with .btn:active { animation: none; }, yet the confirming icon-color rule sits in a separate line outside that animation, so tapping the heart still fills it in even with motion off. 01 is the outlier: the ring is removed entirely at opacity: 0, replaced by nothing more than the small brightness dip its plain :active state already carries.

Focus-visible for keyboard users

Two of the nine also carry a :focus-visible outline so keyboard users get the same signal a pointer would:

.card:focus-visible {
  outline: 3px solid $color;
  outline-offset: 3px;
}

Tap targets sized in CSS

Tap targets are sized in the same CSS rather than left to chance — 05's list rows and 08's checkbox label both carry min-height: 44px, matching the floor WCAG 2.5.8 Target Size (Minimum) sets for anything a finger needs to land on, even though 08's visible checkbox square itself measures only 26px. See the rest of this line of demos on the CSS category hub, or read what this site does and doesn't sell on the about page.

FAQ

Does a css button clicked state need :active, or does pointerdown avoid an iOS delay?

It avoids the delay on purpose. Safari on iOS only applies :active styles right away once some element in the page carries a touchstart listener — without one, a tap-and-release can finish before the style ever paints, a quirk MDN's :active reference notes directly. 01 sidesteps the whole issue by never depending on :active for its ring in the first place; a pointerdown listener swaps the class in immediately on every platform.

Does the touch-only style in 03 still apply on a touchscreen laptop that also has a mouse?

(hover: none) and (pointer: coarse) is judged against the device's primary pointer, not every input it happens to own. A touchscreen laptop that reports touch as primary still gets 03's background; the moment its mouse becomes primary, that block goes quiet on its own. Caniuse's interaction media features table shows Chrome, Safari, and Firefox have all supported this pairing for years.

Can all nine css button click feedback effects sit on one page together?

Yes — each one is scoped to its own class (.btn, .card, .list, .cb, .p__link), so nothing here shares state across effects. The one thing to avoid is stacking two of them on the very same element, such as 02's instant press together with 04's 600ms delay; a button can't cleanly tell a visitor which timing to expect, so pick one feedback style per element instead. A button that has to keep reporting after the finger lifts — spinner, then done — belongs with the button state css patterns rather than any of these nine.

Enter the archive password

The password is inside this article. You will find it as you read.