Skip to content
WANCRAFT

A.64Buttons & Controls

Reaction Button

A like toggle that pops with a spring, fires a ring and particle burst, and rolls its count digit by digit.

NewReactMotionSVGTailwindintermediate

Overview

Reaction Button is the familiar heart (or star, thumb, bookmark, or your own icon) done with care. Adding a reaction squashes the icon and releases it on a spring, while a ring shockwave and a small, deterministic particle burst radiate from it. The count rolls digit by digit from its previous value: up when you react, down when you take it back, and only the digits that change move. It is a native toggle button with aria-pressed and an accessible name that includes the count, works controlled or uncontrolled, and drops every flourish with reduced motion.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/reaction-button.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/reaction-button.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/reaction-button.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { ReactionButton } from "@/components/ui/reaction-button";

export function ReactionButtonExample() {
  return (
    <ReactionButton
      count={128}
      onPressedChange={(pressed, count) => console.log(pressed, count)}
    />
  );
}

03Props

PropTypeDefaultDescription
pressed / defaultPressedboolean— / falseControlled pressed state, or the initial state when uncontrolled.
onPressedChange(pressed: boolean, count: number) => void—Called on toggle with the next state and the suggested next count (current ± 1).
countnumber0Total reactions including the viewer's own when pressed. Uncontrolled buttons add or remove the viewer on top; controlled buttons show it as passed.
labelstring"Like"Accessible name; the formatted count is appended ("Like, 128").
icon"heart" | "star" | "thumb" | "bookmark" | ((pressed: boolean) => ReactNode)"heart"Built-in glyph (outlined → filled) or your own render function.
accentstring"#FF7448"Active icon, tint, ring, particles and focus ring colour.
size"sm" | "md" | "lg""md"Button height, icon and type size.
variant"pill" | "ghost""pill"Bordered capsule or bare icon + count.
showCountbooleantrueShow the rolling count.
burst / particlesboolean / numbertrue / 8Ring shockwave and particle burst on activate, and how many particles (0–24).
formatCount(count: number) => string—Formats the count. Defaults to en-US grouping, compact from 10,000 (12.4K).
...propsButtonHTMLAttributes—Forwarded to the <button>; the ref points to it. Calling preventDefault in onClick cancels the toggle.

04Features

  • 01Spring pop on activate (squash, then release with velocity) and a softer press-back on deactivate
  • 02Ring shockwave plus a deterministic particle burst, remounted per activation
  • 03Digit-wise rolling count that moves up or down from the previous value, never from zero
  • 04Outlined → filled built-in glyphs, or any custom icon via a render function
  • 05Controlled or uncontrolled, with a suggested next count in the callback
motion · springmotion · tweeninput · clickinput · keyboard

05Accessibility

  • Native <button> with aria-pressed, so it is announced as a toggle and works with Enter and Space.
  • The accessible name includes the formatted count ("Like, 128"); the visual digits and burst are aria-hidden.
  • Visible focus ring in the accent colour.
  • With prefers-reduced-motion there is no pop, burst or rolling; the state and count update instantly.