SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/animated-buttons

7 CSS Animated Buttons

0 Pure CSS7 with JSPublished 2026-10-05Free · no sign-up

Seven buttons that animate when you use them: a like button with a burst, a send button whose paper plane flies off, a Follow toggle, an add to cart dot that flies into the cart, confetti on completing a task, a hold-to-delete button and a subscribe button with a ringing bell.

01. Like Button with Burst

AnimatedToggle

Pressing like fills the heart, pops it slightly larger and sends out a ring of small dots, all from CSS on the pressed state. The button is a toggle with aria-pressed and its count updates in the same text, so the state is clear without the burst.

View code
<button class="abt1" aria-pressed="false"><span class="h" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10z"/></svg><i></i></span><span class="c">128</span><span class="sr"> likes</span></button>
.abt1 { display: inline-flex; align-items: center; gap: 8px; padding: 10px 16px; border: 1px solid #e4e7ec; border-radius: 999px; background: #fff; color: #344054; font: 700 15px Inter, system-ui, sans-serif; cursor: pointer; }
.abt1 .h { position: relative; display: grid; width: 24px; height: 24px; }
.abt1 svg { width: 24px; height: 24px; fill: none; stroke: #475467; stroke-width: 2; transition: transform .3s cubic-bezier(.3,1.8,.6,1); }
.abt1 i { position: absolute; inset: -6px; border-radius: 50%; background: radial-gradient(circle, #e11d48 2px, transparent 2.5px) 50% 0 / 6px 6px no-repeat, radial-gradient(circle, #f59e0b 2px, transparent 2.5px) 100% 50% / 6px 6px no-repeat, radial-gradient(circle, #e11d48 2px, transparent 2.5px) 50% 100% / 6px 6px no-repeat, radial-gradient(circle, #f59e0b 2px, transparent 2.5px) 0 50% / 6px 6px no-repeat; opacity: 0; }
.abt1[aria-pressed="true"] svg { fill: #e11d48; stroke: #e11d48; transform: scale(1.15); }
.abt1[aria-pressed="true"] i { animation: abt1 .5s ease-out; }
.abt1[aria-pressed="true"] { color: #be123c; border-color: #fecdd3; }
.abt1 .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
@keyframes abt1 { 0% { opacity: 1; transform: scale(.4); } 100% { opacity: 0; transform: scale(1.4); } }
.abt1:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .abt1 i { animation: none !important; } .abt1 svg { transition: none; } }
var b = document.querySelector('.abt1'), c = b.querySelector('.c');
b.onclick = function () { var on = b.getAttribute('aria-pressed') !== 'true'; b.setAttribute('aria-pressed', on); c.textContent = +c.textContent + (on ? 1 : -1); };

02. Send Button with Paper Plane

AnimatedFeedback

On click the paper plane flies off to the top right, the label changes to Sent, and a moment later everything returns. The flight is a keyframe on the icon only, the label change is real text, and a live region announces Message sent.

View code
<div class="abt2"><button><span class="ic" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 11l18-8-8 18-2-8z"/></svg></span><span class="l">Send</span></button><p class="sr" aria-live="polite"></p></div>
.abt2 button { display: inline-flex; align-items: center; gap: 10px; min-width: 120px; padding: 12px 20px; border: 0; border-radius: 12px; background: #2563eb; color: #fff; font: 700 15px Inter, system-ui, sans-serif; cursor: pointer; overflow: hidden; }
.abt2 svg { width: 20px; height: 20px; fill: #fff; }
.abt2 .ic { display: grid; }
.abt2 .go .ic { animation: abt2 .7s ease-in forwards; }
@keyframes abt2 { 30% { transform: translate(-3px, 3px); } 100% { transform: translate(60px, -40px) rotate(15deg); opacity: 0; } }
.abt2 .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.abt2 button:focus-visible { outline: 3px solid #93c5fd; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .abt2 .go .ic { animation: none; } }
var b = document.querySelector('.abt2 button'), l = b.querySelector('.l'), live = document.querySelector('.abt2 .sr');
b.onclick = function () { if (b.classList.contains('go')) return; b.classList.add('go'); l.textContent = 'Sent'; live.textContent = 'Message sent'; setTimeout(function () { b.classList.remove('go'); l.textContent = 'Send'; }, 1800); };

03. Follow to Following Swap

AnimatedToggle

The Follow label slides up and away as Following with a tick slides in, and turns quiet grey so the pressed state does not shout. Both labels sit in the same grid cell, so the button is always as wide as the longer one and nothing jumps when they swap.

View code
<button class="abt3" aria-pressed="false"><span class="a">Follow</span><span class="b" aria-hidden="true">✓ Following</span></button>
.abt3 { position: relative; display: inline-grid; padding: 10px 18px; border: 0; border-radius: 999px; background: #101828; color: #fff; font: 700 14px Inter, system-ui, sans-serif; cursor: pointer; transition: background .25s, color .25s; }
.abt3 span { grid-area: 1 / 1; transition: opacity .2s, transform .25s; white-space: nowrap; }
.abt3 .b { opacity: 0; transform: translateY(8px); }
.abt3[aria-pressed="true"] { background: #f2f4f7; color: #344054; }
.abt3[aria-pressed="true"] .a { opacity: 0; transform: translateY(-8px); }
.abt3[aria-pressed="true"] .b { opacity: 1; transform: none; }
.abt3:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .abt3, .abt3 span { transition: none; } }
var b = document.querySelector('.abt3');
b.onclick = function () { var on = b.getAttribute('aria-pressed') !== 'true'; b.setAttribute('aria-pressed', on); };

04. Add to Cart Fly-In

AnimatedEcommerce

A dot flies from the Add to cart button into the cart icon along a curve, then the cart count bumps. The curve comes from animating the x and y offsets on two nested elements with different easings, a classic trick for arcs without paths.

View code
<div class="abt4"><a href="#cart" class="cart" aria-label="Cart, 0 items">🛒<span class="n" aria-hidden="true">0</span></a><button>Add to cart</button></div>
.abt4 { position: relative; display: flex; flex-direction: column; align-items: center; gap: 70px; width: 240px; max-width: 100%; font-family: Inter, system-ui, sans-serif; }
.abt4 .cart { position: relative; align-self: flex-end; display: grid; place-items: center; width: 46px; height: 46px; border-radius: 12px; background: #1e293b; font-size: 20px; text-decoration: none; }
.abt4 .n { position: absolute; top: -6px; right: -6px; min-width: 18px; height: 18px; border-radius: 9px; background: #22c55e; color: #052e16; font: 800 11px/18px Inter, system-ui, sans-serif; text-align: center; }
.abt4 .n.bump { animation: abt4n .4s ease; }
.abt4 button { padding: 12px 20px; border: 0; border-radius: 10px; background: #22c55e; color: #052e16; font: 800 14px Inter, system-ui, sans-serif; cursor: pointer; }
.abt4 .fx { position: absolute; left: 50%; bottom: 20px; animation: abt4x .6s ease-in forwards; }
.abt4 .fx i { display: block; width: 14px; height: 14px; border-radius: 50%; background: #22c55e; animation: abt4y .6s cubic-bezier(.2,-.6,.4,1) forwards; }
@keyframes abt4x { to { transform: translateX(95px); } }
@keyframes abt4y { to { transform: translateY(-100px) scale(.5); } }
@keyframes abt4n { 50% { transform: scale(1.4); } }
.abt4 :focus-visible { outline: 3px solid #86efac; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .abt4 .fx { display: none; } .abt4 .n.bump { animation: none; } }
var w = document.querySelector('.abt4'), n = w.querySelector('.n'), cart = w.querySelector('.cart'), k = 0;
w.querySelector('button').onclick = function () {
  var fx = document.createElement('span'); fx.className = 'fx'; fx.innerHTML = '<i></i>'; fx.setAttribute('aria-hidden', 'true'); w.appendChild(fx);
  setTimeout(function () { fx.remove(); k++; n.textContent = k; cart.setAttribute('aria-label', 'Cart, ' + k + (k === 1 ? ' item' : ' items')); n.classList.remove('bump'); void n.offsetWidth; n.classList.add('bump'); }, matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 600);
};

05. Confetti Celebration Button

AnimatedCelebration

Completing a task sprays a short burst of confetti from the button. The pieces are a dozen small spans given random directions through custom properties, removed after one second, and skipped entirely under reduced motion.

View code
<div class="abt5"><button>Mark as done</button></div>
.abt5 { position: relative; display: grid; place-items: center; width: 260px; max-width: 100%; height: 180px; overflow: hidden; }
.abt5 button { position: relative; z-index: 1; padding: 13px 22px; border: 0; border-radius: 12px; background: #7c3aed; color: #fff; font: 700 15px Inter, system-ui, sans-serif; cursor: pointer; }
.abt5 button.done { background: #067647; }
.abt5 .cf { position: absolute; left: 50%; top: 50%; width: 8px; height: 12px; background: var(--c); animation: abt5 1s cubic-bezier(.1,.7,.3,1) forwards; }
@keyframes abt5 { to { transform: translate(var(--x), var(--y)) rotate(var(--r)); opacity: 0; } }
.abt5 button:focus-visible { outline: 3px solid #c4b5fd; outline-offset: 3px; }
var w = document.querySelector('.abt5'), b = w.querySelector('button'), cols = ['#f43f5e', '#f59e0b', '#22c55e', '#3b82f6', '#a855f7'];
b.onclick = function () {
  var done = b.classList.toggle('done'); b.textContent = done ? 'Done ✓' : 'Mark as done';
  if (!done || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  for (var i = 0; i < 14; i++) { var s = document.createElement('span'), a = Math.random() * Math.PI * 2, d = 60 + Math.random() * 60; s.className = 'cf'; s.setAttribute('aria-hidden', 'true');
    s.style.cssText = '--c:' + cols[i % 5] + ';--x:' + Math.cos(a) * d + 'px;--y:' + Math.sin(a) * d + 'px;--r:' + (Math.random() * 720 - 360) + 'deg';
    w.appendChild(s); setTimeout(s.remove.bind(s), 1000); }
};

06. Press and Hold to Confirm

AnimatedDestructive

A destructive button that only fires after being held for a second and a half, with a fill showing progress. It works with the pointer and with Space or Enter held on the keyboard, and releasing early cancels. The instruction is in the button's description.

View code
<div class="abt6"><button aria-describedby="abt6h"><span class="f" aria-hidden="true"></span><span class="l">Hold to delete</span></button><p id="abt6h">Press and hold for 1.5 seconds.</p></div>
.abt6 { display: grid; justify-items: center; gap: 8px; font-family: Inter, system-ui, sans-serif; }
.abt6 button { position: relative; overflow: hidden; isolation: isolate; width: 200px; max-width: 100%; padding: 13px; border: 1px solid #f87171; border-radius: 10px; background: #1f1215; color: #fecaca; font: 700 15px Inter, system-ui, sans-serif; cursor: pointer; user-select: none; }
.abt6 .f { position: absolute; inset: 0; z-index: -1; background: #b91c1c; transform: scaleX(0); transform-origin: left; }
.abt6 button.hold .f { transform: scaleX(1); transition: transform 1.5s linear; }
.abt6 button.ok { background: #b91c1c; color: #fff; }
.abt6 p { margin: 0; color: #94a3b8; font-size: 12px; }
.abt6 button:focus-visible { outline: 2px solid #fca5a5; outline-offset: 3px; }
var b = document.querySelector('.abt6 button'), l = b.querySelector('.l'), t;
function start(e) { if (e.type === 'keydown' && (e.repeat || (e.key !== ' ' && e.key !== 'Enter'))) return; if (e.type === 'keydown') e.preventDefault(); if (b.classList.contains('ok')) return; b.classList.add('hold'); t = setTimeout(function () { b.classList.remove('hold'); b.classList.add('ok'); l.textContent = 'Deleted'; }, 1500); }
function stop() { clearTimeout(t); b.classList.remove('hold'); }
b.addEventListener('pointerdown', start); b.addEventListener('keydown', start);
['pointerup', 'pointerleave', 'keyup', 'blur'].forEach(function (ev) { b.addEventListener(ev, stop); });
b.addEventListener('click', function (e) { e.preventDefault(); });

07. Subscribe with Ringing Bell

AnimatedToggle

Subscribing turns the button solid and gives its bell a short ring, a satisfying confirmation borrowed from video platforms. The ring plays once when the pressed state becomes true and never loops.

View code
<button class="abt7" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 16V11a6 6 0 1 1 12 0v5l2 2H4zM10 20a2 2 0 0 0 4 0"/></svg><span>Subscribe</span></button>
.abt7 { display: inline-flex; align-items: center; gap: 8px; padding: 11px 18px; border: 1px solid #d0d5dd; border-radius: 999px; background: #fff; color: #101828; font: 700 14px Inter, system-ui, sans-serif; cursor: pointer; transition: background .2s, color .2s; }
.abt7 svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linejoin: round; transform-origin: 50% 10%; }
.abt7[aria-pressed="true"] { background: #101828; color: #fff; border-color: #101828; }
.abt7[aria-pressed="true"] svg { fill: currentColor; animation: abt7 .8s ease; }
@keyframes abt7 { 15% { transform: rotate(18deg); } 30% { transform: rotate(-16deg); } 45% { transform: rotate(10deg); } 60% { transform: rotate(-6deg); } 75% { transform: rotate(2deg); } }
.abt7:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .abt7 svg { animation: none !important; } }
var b = document.querySelector('.abt7'), s = b.querySelector('span');
b.onclick = function () { var on = b.getAttribute('aria-pressed') !== 'true'; b.setAttribute('aria-pressed', on); s.textContent = on ? 'Subscribed' : 'Subscribe'; };

About these CSS Animated Buttons

Animate the result, not the click

These animations confirm that something happened: liked, sent, added, deleted. Run them after the state changes and pair them with a visible change in text or icon fill that persists.

Toggles report their state

Like, follow and subscribe are toggles. aria-pressed tells assistive technology whether they are on, and CSS can style the pressed state directly from the attribute.

Hold-to-confirm needs a keyboard path

Long-press buttons must also work by holding Space or Enter, and say how long to hold. Releasing early should cancel without side effects.

Keep particles disposable

Confetti and bursts are short-lived spans: create them, let them animate, then remove them. Skip them entirely under prefers-reduced-motion, since the state change is what matters.

Frequently asked questions

Do these buttons do anything real?

They show the interaction only. Wire them to your own requests and keep the animation on success.

Can I use these with React or Vue?

Yes. Toggle the same attributes and classes from component state.

Is confetti too much?

For a milestone, no. For every click, yes; keep celebrations for genuine achievements.