ui-025

Animated Like Button

£10 or the whole Vault, £99

An animated like button: the icon springs, a particle burst fires and the count rolls over one digit at a time.

intermediate
5 more details
micro-interactionclick-stateparticle-poolingspring-physicsodometer-counter

This element reads better at your own screen size than in the frame below:

Updated

About this component

A reaction button that answers the press. The icon fills and springs back on an elastic ease, a ring pulses out from behind it, eleven particles fly out on their own angles and fade, and the count rolls over instead of swapping. Press it again and the whole thing reverses on a quick flat shrink, so undoing never reads as another celebration.

Read the full effect overview

The particles are pooled spans, not a canvas: eleven elements are created once, appended for the burst, removed when their tween ends and handed back to a pool for the next press. The demo shows the chips where they belong, on a short run of message rows, and can drop to a single reaction per row to show the component solo. One HTML file, one stylesheet, one script, GSAP core only, and a light and a dark theme switched by a data-variant attribute.

Animated Like Button - UI component preview

What's included

10 items
  • Icon springs back on an elastic ease, with a ring pulse behind it
  • Radial burst of eleven pooled DOM particles, no canvas and no library
  • Particles are removed from the DOM on completion and reused on the next press
  • Count rolls one digit up on press and back down on un-press
  • Un-press is a short flat shrink, not a second celebration
  • Per-reaction accent colour read from a custom property on the button
  • Reduced motion skips the particles entirely and flips the state instantly
  • Real buttons with aria-pressed and a live count in the accessible name
  • window.reactionBurst.toggle / count / reset for your own code
  • Light and dark themes, every colour a custom property

Perfect for

4 use cases
  • Post and comment reactions in social feeds and communities
  • Article likes, claps and saves on editorial sites
  • Upvotes and favourites in dashboards and internal tools
  • Any counter currently incrementing with no acknowledgement at all

How it works

4 sections

Pooled particles, not a canvas

A burst asks a pool array for eleven spans, creating one only when the pool is empty. Each is appended to a zero-size origin point at the centre of the icon, given an angle spread evenly around the circle with a small random jitter and a random distance, then tweened out on x and y while its scale and opacity fall away. On completion the span is removed from the DOM and pushed back to the pool, so a hundred presses still allocate eleven elements.

The spring is the ease, not a physics loop

The press sets the icon to a scale of 0.55 and tweens it to 1 on elastic.out(1, 0.42). That single ease carries the overshoot and the settle, which is why the whole gesture costs one tween. Nothing in the CSS sets a transform on the icon, the ring or a particle, so every scale, x and y starts from a real zero.

Rolling one digit

The count is a clipped box with two stacked spans. The incoming value is written into the hidden one, placed a full height below (or above, when the count goes down) with yPercent, and the two are tweened past each other. When the tween lands the visible span takes the new text, both spans are reset to their resting offsets and the hidden one is emptied, so the next roll starts from a clean stack.

Colours come from the theme

Each button carries its own --rx-accent, mapped in one rule per reaction to the theme's like, star and clap colours: the accent for the like, two muted status tones for the others so a row of three never reads as confetti. The script reads that property with getComputedStyle on the button at the moment a burst fires, so particles and the ring are always the right colour and a fourth reaction needs one CSS rule and no JavaScript. Under prefers-reduced-motion the burst and the ring are skipped and the state flips with no motion at all.

Difficulty Intermediate
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this UI element's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid UI element

Purchase to unlock the code.

Buying Animated Like Button opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

£10 Standard license, unlimited projects

Browse UI elements

Or the whole library: the Vault, £99, one payment.

Documentation

What's Included

  • index.html: the demo page and the markup to copy

  • assets/style.css: component styles, every colour and size a plain CSS custom property

  • assets/script.js: readable, commented source with an onReady guard

  • Icon springs back on an elastic ease, with a ring pulse behind it

  • Radial burst of eleven pooled DOM particles, no canvas and no library

  • Particles are removed from the DOM on completion and reused on the next press

  • Count rolls one digit up on press and back down on un-press

  • Un-press is a short flat shrink, not a second celebration

  • Reduced motion skips the particles entirely and flips the state instantly

  • Light and dark themes, every colour a custom property

  • window.reactionBurst.toggle / count / reset for your own code

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

The inline line only gates the ring's hidden start state, so a no-JavaScript visitor never sees a stray circle behind the icon.

2. Add to your <body>:

Copy a <div class="rx-row"> block out of index.html. Each reaction is one <button class="rx-btn" data-reaction="..."> holding an .rx-icon (with its .rx-burst origin, its .rx-ring and an SVG glyph), an .rx-name and an .rx-count stack. Use one button or ten, in one row or in many: the script wires every .rx-btn it finds inside the #rxRow container.

The message rows the demo hangs the chips on (.stage-msg, the avatar, the name and the time) are demo furniture, not part of the component. So are the toolbar across the top and its two controls. Delete them and put the .rx-row wherever your own product needs it; only the #rxRow container around the buttons has to survive.

3. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

4. Drive it, or read it, from your own code:

Code snippet omitted: it ships with the download.

Seed a real count by putting it on the button and in the visible span before the script runs:

Code snippet omitted: it ships with the download.

Post the new value to your API from inside the click handler in press(); the animation does not wait on it, which is what makes the button feel instant.

Using It With Your Own Design

What the component actually requires:

  • A .rx-icon with position: relative, holding a zero-size .rx-burst pinned at its centre. That point is the burst origin; move it and the particles start somewhere else.
  • An .rx-count with overflow: hidden and a fixed height, holding .rx-count-now in flow and .rx-count-next absolutely on top of it. The roll is only those two elements passing each other.
  • A --rx-accent custom property on each button. One rule per data-reaction value maps it to whatever colour that reaction should throw.
  • Nothing in your CSS may set a transform on the button, the icon, the ring or a particle. GSAP animates scale, x, y and yPercent on those, and a CSS transform is parsed into the same values and fights them.

What is only the demo's dressing and can be deleted: the .rx-name labels, the status line, the message rows and the toolbar controls. Swap the SVG glyphs for your own freely; the stroke-to-fill switch is one CSS rule keyed on .is-on, and any open path you do not want filled just needs the rx-spark class.

How It Works

Pooled particles, not a canvas. A burst asks a pool array for eleven spans, creating one only when the pool is empty. Each is appended to the zero-size origin at the centre of the icon, given an angle spread evenly around the circle with a small random jitter and a random distance, then tweened out on x and y while scale and opacity fall away. On completion the span is removed from the DOM and pushed back to the pool, so a hundred presses still allocate eleven elements.

The spring is the ease, not a physics loop. The press sets the icon to scale: 0.55 and tweens it to 1 on elastic.out(1, 0.42). That single ease carries the overshoot and the settle, which is why the whole gesture costs one tween.

Rolling one digit. The count is a clipped box with two stacked spans. The incoming value is written into the hidden one, placed a full height below (or above, when the count goes down) with yPercent, and the two are tweened past each other. When the tween lands the visible span takes the new text, both are reset to their resting offsets and the hidden one is emptied, so the next roll starts from a clean stack.

Colours come from the variant. Each button carries its own --rx-accent. The script reads it with getComputedStyle on the button at the moment a burst fires, never from a literal in the code, so switching data-variant mid-session produces the right particles on the next press.

Themes

One design at two token values. Set the attribute on <body> in the demo, or on any ancestor of the component in your own page:

Code snippet omitted: it ships with the download.

Nothing else changes: same markup, same script, and nothing in the JavaScript ever reads the theme name. The demo's toolbar toggle and the ?variant=light URL parameter only set that attribute.

Every colour the component and the demo ground use is a custom property defined in both body[data-variant="light"] and body[data-variant="dark"] in assets/style.css:

Token What it colours
--ground, --ground-2 the page and the toolbar strip (demo only)
--raised, --hover the chip at rest and on hover, the message cards
--ink, --ink-2, --ink-3 headings, body copy, chip labels and counts
--line, --line-strong hairlines and the chip border
--accent, --accent-ink selection and, in dark, the focus ring
--focus the :focus-visible outline
--rx-heart, --rx-star, --rx-clap the glyph, ring and particles of each reaction
--particle-size particle box size

To re-value it for your own brand, change those properties in the two blocks; no selector override is needed anywhere. The like reaction carries the accent and the other two are muted status tones, which is what keeps a row of three from reading as confetti.

Customisation

Property Where Default Description
--rx-heart style.css #f5c400 (dark) Accent for the like reaction, used by its glyph, particles and ring
--rx-star style.css #56b088 (dark) Accent for the star reaction
--rx-clap style.css #7ea8de (dark) Accent for the clap reaction
--particle-size style.css 6px Particle box size
--focus style.css #f5c400 (dark) Focus outline colour
PARTICLES script.js 11 Particles per burst, and therefore the pool's size
SPREAD script.js 30 Base travel distance in pixels, before the random extra

The spring is the elastic.out(1, 0.42) ease in press(); drop the second number for a longer wobble. The un-press shrink is the 0.78 scale on the line below it.

Accessibility

Keyboard and ARIA contract. Each reaction is a native <button> with aria-pressed reflecting whether the reaction is on. Tab reaches every chip in document order; Enter and Space press it, exactly as a click does, and the burst plus the count roll run either way. The accessible name is rewritten on every change ("Like, 13 reactions"), and a role="status" line announces the change politely. There is no roving tabindex and no custom key handling to conflict with your own.

  • Every reaction is a real <button> carrying aria-pressed, so the on and off states are announced rather than only coloured.
  • The accessible name is rewritten on every change to "Like, 1 reaction", so the count is spoken with the control instead of being a loose number beside it.
  • A role="status" line announces the change as well, for screen readers that do not re-read a pressed state.
  • Particles and the ring are aria-hidden decoration; nothing about the state depends on them.
  • Under prefers-reduced-motion: reduce the burst and the ring are skipped entirely and every duration collapses to zero, so the state flips with no motion.
  • With JavaScript off the buttons are still buttons and the counts are still readable; nothing is hidden behind a class that never arrives.
  • Focus is visible on the buttons and on the demo toolbar controls via :focus-visible outlines.

Requirements

  • GSAP 3.12+ (core only, no plugins)
  • No build step, no framework

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.