Reactions
Reactions let people respond to content with an emoji in one tap. They sit inline under the content or float in a corner of the page.
Just shipped the reactions component. Picking one turns a click into a small celebration, which is the whole point.
Features
- Inline pills under the content, or a floating bar pinned to a corner of the viewport.
- One tap to react or take it back, with a particle burst. Counts past a thousand shorten to 1.2K.
- A picker of six common reactions, plus any already on the item.
- Controlled or uncontrolled. Particles and pill motion switch off under reduced motion.
Installation
npx shadcn@latest add https://fibo.toribryan.com/r/reactions.jsonimport { Reactions, type Reaction } from "@/components/ui/reactions"No animation library: the burst and the pill motion use the Web Animations API.
Usage
import { Reactions } from "@/components/ui/reactions"
export function MessageReactions() {
return (
<Reactions
defaultReactions={[
{ emoji: "👍", label: "Thumbs up", count: 5 },
{ emoji: "❤️", label: "Heart", count: 3, active: true },
]}
/>
)
}A reaction's emoji is its identity, so keep it unique. Its label is what
screen readers hear.
Anatomy
Every part the component renders, with the data-slot it carries. You pass
the reactions; the component builds the rest.
| Part | data-slot | Notes |
|---|---|---|
| Reactions | reactions | The root. Carries data-variant. |
| pill | reactions-pill | Inline only. One toggle per reaction. |
| emoji | reactions-pill-emoji | |
| count | reactions-pill-count | While showCounts is on. |
| picker | reactions-picker | Holds the trigger and the panel. |
| panel | reactions-panel | The choices, while the picker is open. |
| choice | reactions-choice | One per choice. |
| badge | reactions-badge | Floating only. The total count. |
| trigger | reactions-trigger | Opens and closes the panel. |
The particles fly in one fixed layer, reactions-particles, added to
document.body on the first burst so no container clips them.
Guidelines
- 01Use inline beside the content it belongs to, such as a message, comment or post.
- 02Use floating for a page-level reaction, such as the end of an article. It pins to a viewport corner.
- 03Offer five or six choices at most. More turns a quick tap into a decision.
- 04Leave
choicesunset to get six common reactions. Anything already on the item is added to them. - 05A reaction nobody holds any more disappears instead of lingering as a zero.
- 06Counts past a thousand shorten to 1.2K, so a busy post keeps its pills small.
Examples
Inline
The default: pills under the content, with the picker's trigger at the end.
No reactions yet
With nothing on the item, only the trigger shows.
Busy post
Counts past a thousand shorten, such as 1.2K, so the pills stay small.
Without counts
showCounts={false} hides the numbers.
Floating
variant="floating" pins a bar to a viewport corner, chosen with position.
The floating variant is fixed to a corner of the viewport and stays put as the page scrolls. It sits on the same translucent bar the site nav uses, clears the safe area on a notched phone, and counts every reaction on the item beside its trigger. Particles rise from the bar itself rather than from the emoji that was picked.
Controlled
reactions and onReactionsChange hand the state to your data layer.
9 reactions across 3 kinds
Do's and don'ts
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "inline" | "floating" | "inline" | Pills under the content, or a bar pinned to a viewport corner. |
position | "bottom-right" | "bottom-left" | "top-right" | "top-left" | "bottom-right" | Viewport corner. Applies to the floating variant only. |
reactions | Reaction[] | — | The reactions on the item. Pass it to control the state yourself. |
defaultReactions | Reaction[] | [] | The starting reactions when uncontrolled. |
choices | Reaction[] | — | What the picker offers. Defaults to six common reactions plus any already on the item. |
onReactionsChange | (reactions: Reaction[]) => void | — | Fires with the whole new list after every change. |
onReact | (reaction: Reaction, active: boolean) => void | — | Fires with the reaction that changed and whether it is now on. |
showCounts | boolean | true | Show the count on each pill. |
particles | number | 7 | Emoji thrown up on each new reaction. 0 turns the burst off. |
triggerLabel | string | "Add reaction" | Accessible name of the button that opens the picker. |
panelLabel | string | "Pick a reaction" | Accessible name of the group of choices. |
A Reaction is { emoji, label, count?, active? }: the emoji is its identity, the label is what screen readers hear, and active marks whether the current person has reacted with it.
Data attributes
| Attribute | Element | Present when |
|---|---|---|
data-slot="reactions" | The root | Always. |
data-variant | The root | Always. inline or floating. |
data-emoji | Each pill | Always. The pill's emoji. |
data-active | Each pill and choice | The current person has reacted with it. |
data-state | The trigger, the panel and each choice | Always. open or closed. |
Accessibility
- 01Each pill is a toggle button named by the reaction's label and count, such as "Heart, 3 reactions", with
aria-pressedfor whether you've reacted. The emoji itself is hidden from screen readers. - 02A polite live region announces each change: "Added Heart", "Removed Heart".
- 03Choices you've already picked show as pressed in the picker too.
- 04Arrow keys, Home and End move through the choices. Escape closes the picker and returns focus to its trigger; tabbing away closes it too.
- 05Particles and pill motion switch off under prefers-reduced-motion.
- 06Override
triggerLabelandpanelLabelwhen your product speaks another language.
Related components
- Token flowWalks a colour token from raw value to primitive to semantic role.
- Chapter scrubberA rail of marks that swell under the pointer like the Dock, previewing the chapter at the crest.
- BadgeIn fibo's Storybook
References
- WAI-ARIA button pattern, for the toggle pills
- Web Animations API
