Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Buttons

Pro

Counter Button for React

A button with a number that rolls to its next value on each press, for likes, votes and carts. For React with Base UI, when a count should feel like it moved.

Framework
UI library

Each press adds incrementBy to the number beside the label, and every digit that changes rolls to its new value like an odometer, while the others stay still.

Use it for likes, upvotes, add-to-cart and guest counts: small numbers someone nudges with a press. For a number people type or set precisely, use a number field instead. It wraps the Button, so variant and size work as usual. Each digit is a column of 0 to 9 behind a one-digit window, slid by a CSS transition, with no animation library; with reduced motion the digits change in place without rolling.

Preview width

Installation

Part of Pro
Pro unlocks the code and one-line install of every Pro button and text effect, plus the MCP for your AI editor.
Have Pro already? Sign in to open it.Opens here as soon as you're Pro.

Usage

import { CounterButton } from "@/components/ui/counter-button";
<CounterButton defaultCount={128}>Like</CounterButton>

Demos

Step

incrementBy is what one press adds, and the default is 1. A box of 5 adds 5 at a time; a negative step counts down, as a downvote does. Here min stops the downvotes at 0.

Preview width

Controlled

Keep the number in your own state: pass it as count and update it from onCountChange. Here a Clear button sets it back to 0 and the digits roll down to it, and max stops the guest list at 6.

Preview width

Accessibility

The rolling digits are hidden from assistive tech. The button carries the plain number instead, in a polite status inside it, so it's announced as its label followed by the count, such as Like 128, and each new count is read out after a press. It presses with Enter and Space like any button.

Enter
Add one step to the count.
Space
Add one step to the count.

FAQ

How do I start from an existing count?

Pass defaultCount, such as the number of likes you loaded. It's 0 by default, and the button keeps counting from there on its own. To hold the number yourself, pass count and onCountChange instead, as in the Controlled demo; the digits roll whenever count changes.

How do I save the new number?

Use onCountChange: it gets the new number after each press that changes it. onClick still gets the click event as usual.

Can it count down?

Yes. Give incrementBy a negative number, such as -1 for a downvote, and min={0} to stop at zero. Each digit rolls to its new value whichever way the count goes.

How do I stop it at a maximum?

Pass max, and min for a floor. A press that would go past either stops at it, and a press at the limit does nothing. To show that, disable the button once your stored count reaches the limit. Pass disabled.

How do I put a heart next to the label?

Pass it as icon: it goes before the label, and the number after. The preview at the top of this page uses a heart.

Does the button get wider as the number grows?

Only when it gains a digit, as 99 becomes 100. The digits use tabular figures, so changing one digit never shifts the others.

What happens with reduced motion?

The digits switch to the new number without rolling. The count and the press feedback are the same.

API Reference

CounterButton

data-slot="counter-button-count"

PropType
count?number

The number it shows, when you hold it yourself. Pair it with onCountChange.

defaultCount?number

The number it starts from when you don't pass count.

Default
0
onCountChange?(count: number) → void

Called with the new number after each press that changes it.

Parameters

count – number

Returns
void
children?ReactNode

The label before the count.

focusableWhenDisabled?boolean

Whether the button should be focusable when disabled.

Default
false
icon?ReactNode

An icon before the label, such as a heart or a cart.

incrementBy?number

What one press adds. A negative step counts down.

Default
1
max?number

The highest number a press can reach; a press past it stops there.

min?number

The lowest number a press can reach; a press past it stops there.

nativeButton?boolean

Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).

Default
true
render?ReactElement | ((props: object, state: object) => ReactElement)

Replace the host element or compose it with another component.

Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string

Merged with the part's classes.

CSS variableType
--counter-button-digitstring

The digit a column shows, 0 to 9 (set per digit).

All 35 buttons