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.

TB
Tori Bryan12:45 PM

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

Installnpx shadcn@latest add https://fibo.toribryan.com/r/reactions.json
Importimport { 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.

Partdata-slotNotes
ReactionsreactionsThe root. Carries data-variant.
pillreactions-pillInline only. One toggle per reaction.
emojireactions-pill-emoji
countreactions-pill-countWhile showCounts is on.
pickerreactions-pickerHolds the trigger and the panel.
panelreactions-panelThe choices, while the picker is open.
choicereactions-choiceOne per choice.
badgereactions-badgeFloating only. The total count.
triggerreactions-triggerOpens 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 choices unset 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

DoKeep reactions right under the content they respond to.
Don'tDon't let every emoji become a pill. A wall of reactions reads as noise; offer fewer choices.

API reference

PropTypeDefaultDescription
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.
reactionsReaction[]—The reactions on the item. Pass it to control the state yourself.
defaultReactionsReaction[][]The starting reactions when uncontrolled.
choicesReaction[]—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.
showCountsbooleantrueShow the count on each pill.
particlesnumber7Emoji thrown up on each new reaction. 0 turns the burst off.
triggerLabelstring"Add reaction"Accessible name of the button that opens the picker.
panelLabelstring"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

AttributeElementPresent when
data-slot="reactions"The rootAlways.
data-variantThe rootAlways. inline or floating.
data-emojiEach pillAlways. The pill's emoji.
data-activeEach pill and choiceThe current person has reacted with it.
data-stateThe trigger, the panel and each choiceAlways. 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-pressed for 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 triggerLabel and panelLabel when your product speaks another language.

References