Skip to main content

Chat

ReactionBar

A message's reactions: each one's emoji and how many chose it, with the person's own pressed and tinted, and quick reactions to add with a single press. Pressing a reaction calls onReact with its key, so the app adds the person's reaction or takes it back. Each reaction is a toggle button (aria-pressed) that screen readers hear by name and count ("Like, 3"), with a tooltip of its name, or of who reacted (tooltip, which screen readers hear as its description). Without onReact the reactions are read-only, and with nothing to show it renders nothing. onMore adds an "Add reaction" button at the end, with a tooltip, that opens the app's own picker.

On this site

This site's own pages use ReactionBar too. Open one with X-ray pointing it out: The Nautilus's story

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/reaction-bar#v1.3.10

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/reaction-bar

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/reaction-bar#v1.3.10 ourostack/ouro-ui/reaction-bar-tests#v1.3.10
import { ReactionBar } from "@/components/ouro/reaction-bar/reaction-bar";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<ReactionBar reactions={message.reactions} quick={quickReactions} onReact={(key) => toggleReaction(message.id, key)} />
<ReactionBar reactions={message.reactions} onReact={react} onMore={(event) => openPicker(event.currentTarget)} moreOpen={pickerOpen} />
<ReactionBar catalogue={catalogue} reactions={[{ key: "party", count: 2, mine: true }]} quick={["like", "heart"]} onReact={react} />

Usage

With catalogue, reactions go by emoji id and show the catalogue's images, as EmojiPicker with the same catalogue chooses them.

Place it in Message's attachments.

Use instead when

  • Use ToggleGroup instead for a choice between options, and Badge for a single count.

Props

Type: ReactionBarProps.

catalogue
Type: EmojiCatalogue
An image emoji catalogue, the same as the app's EmojiPicker: reactions and quick reactions then go by emoji id, each showing the catalogue's image, named by the emoji's name, an animated one playing while its button is under the mouse or in focus.
className
Type: string
Layout only: margins, placement (mt-1). Appearance classes warn in development.
disabled
Type: boolean
Keeps the counts but turns off reacting, for example while the app is offline.
label
Type: string
Default: "Reactions"
What screen readers hear for the group, for other languages.
mineLabel
Type: string
Default: "including you"
What screen readers hear after a read-only reaction that is the person's own.
moreLabel
Type: string
Default: "Add reaction"
The "Add reaction" button's name and tooltip, for other languages.
moreOpen
Type: boolean
Whether the picker that onMore opens is open, so the "Add reaction" button says so (aria-expanded).
onMore
Type: ((event: MouseEvent<HTMLElement>) => void)
Called when the "Add reaction" button at the end is pressed, to open the app's own reaction picker. The button shows only with onMore. Anchor the picker to event.currentTarget, and pass moreOpen while it is open.
onReact
Type: ((key: string, event: MouseEvent<HTMLElement>) => void)
Called with a reaction's key when it is pressed, to add the person's reaction or take it back. The click event comes second, so the app can check event.isTrusted before it acts. Without it the reactions are read-only.
quick
Type: readonly (ReactionOption | string)[]
Reactions offered for a single press, after the counts: each with its own emoji, or, with catalogue, an emoji's id. One that is already in reactions shows there instead, and an id the catalogue lacks is left out. Shown only with onReact.
reactions (required)
Type: readonly Reaction[] | readonly CatalogueReaction[]
The message's reactions, in the order they show: each with its own emoji, or, with catalogue, by emoji id.
ref
Type: Ref<HTMLDivElement>
Reaches the rendered element.

Native attributes

Also accepts the attributes in React's HTMLAttributes (id, aria-*, event handlers), passed to the rendered element, except those named on this page.

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • items: Not a prop. The reactions are reactions: <ReactionBar reactions={message.reactions}>.
  • onAddReaction: Not a prop. The "Add reaction" button calls onMore: <ReactionBar onMore={openPicker}>.
  • onReactionClick: Not a prop. A pressed reaction calls onReact: <ReactionBar onReact={(key) => react(key)}>.

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: ReactionBar merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.

Alignment classes (text-center) warn too: set alignment on a parent element.

Types

CatalogueReaction

A reaction on a message, by the emoji it is, for a ReactionBar with a catalogue.

interface CatalogueReaction {
  /** The emoji's id in the catalogue, or a tone's id (`"thumbs-up-tone3"`); `onReact` receives it. */
  key: string;
  /** How many people reacted this way. */
  count: number;
  /** The person using the app is one of them: the reaction is pressed and tinted. */
  mine?: boolean | undefined;
  /** What screen readers hear for it. @default the emoji's name in the catalogue */
  label?: string | undefined;
  /**
   * Who reacted, shown in a tooltip on hover and keyboard focus, which screen readers hear as its
   * description: "You and Riley Example". A tooltip never opens on touch, so keep the count
   * meaningful without it. Without it, the tooltip shows the reaction's name.
   */
  tooltip?: string | undefined;
  /**
   * What it shows when the catalogue has no emoji with this id, such as an emoji the catalogue dropped. @default the id as a `:token:`
   */
  emoji?: ReactNode;
}

EmojiCatalogue

An image-based emoji set, by category: what EmojiPicker, ReactionBar, Autocomplete and EmojiText draw instead of the platform's emoji font when an app passes it. Put an organization's custom emoji in a category of their own, labelled by the app. The image addresses are used as they are, so an app that loads images its own way (a content security policy, a custom scheme, signed or blob: URLs) puts the addresses it can load in the catalogue when it builds it. Ouro indexes a catalogue's emoji once, to find them in text. Build the catalogue once and pass that object, or give each copy the same id: a catalogue of new emoji objects on every render is indexed again each time, and warns in development. Adding or removing emoji is seen in a new catalogue object, even in the same array; to edit an emoji's shortcodes or text, pass a new emoji object, or a new id.

interface EmojiCatalogue {
  /**
   * Names this set of emoji, for an app that makes a new catalogue object, emoji and all, each
   * time it draws: catalogues with the same id share one index of their emoji, so change it when
   * the emoji change (`"contoso-2026-11"`), an emoji edited in place included. Without one, a
   * new catalogue object shares the index when it holds the same emoji objects in the same order,
   * and a catalogue of new emoji objects builds its own.
   */
  id?: string | undefined;
  /** The categories, in the order they show. An emoji in more than one counts once, by its id. */
  categories: readonly EmojiCatalogueCategory[];
}

EmojiCatalogueCategory

A category of a catalogue: its name, the icon its button shows, and its emoji in order.

interface EmojiCatalogueCategory {
  /** Identifies the category, unique in the catalogue: `"people"`, `"custom"`. */
  id: string;
  /** Its name, shown as its heading and read for its button: `"People"`, or the organization's name for its custom emoji. */
  label: string;
  /** What its button in a picker's category bar shows: an icon element. Defaults to its first emoji's image. */
  icon?: ReactNode;
  /** Its emoji, in the order they show. */
  items: readonly EmojiItem[];
}

EmojiFilmstrip

An animation drawn as a filmstrip: square frames stacked top to bottom in one image, so the frames are the image's height over its width.

interface EmojiFilmstrip {
  /** The filmstrip image's address. */
  src: string;
  /** Frames a second. */
  fps: number;
}

EmojiItem

One emoji in an image catalogue: its id, its name and the image that draws it. An app maps its own emoji set to this, whether a standard set or an organization's custom emoji.

interface EmojiItem {
  /** Identifies the emoji, unique in the catalogue: `"party"`. Reactions, recents and `:id:` tokens use it. */
  id: string;
  /** Its name, which is the image's `alt`, what screen readers hear and what the search matches first: `"Party popper"`. */
  name: string;
  /**
   * The still image's address. It shows whenever the emoji is not playing, and always with reduced motion.
   */
  src: string;
  /** Sizes of the still image for sharper screens, as `<img srcset>`: `"party-2x.png 2x"`. */
  srcSet?: string | undefined;
  /**
   * An animated image (GIF, animated WebP or PNG), which plays while the pointer is on the emoji
   * or on the button around it, or that button has focus. With reduced motion it never plays.
   */
  animatedSrc?: string | undefined;
  /**
   * The animation as a filmstrip, square frames stacked top to bottom in one image, for an
   * emoji without `animatedSrc`: while the emoji plays, it steps through the frames at `fps`;
   * otherwise, and always with reduced motion, `src` shows.
   */
  filmstrip?: EmojiFilmstrip | undefined;
  /** The still image's width in pixels, given to the `<img>` so the page keeps its shape while it loads. It shows at the text's size. */
  width?: number | undefined;
  /** The still image's height in pixels, as `width`. */
  height?: number | undefined;
  /** Short names typed between colons, without them: `["party", "tada"]`. The first makes the emoji's token, `:party:`. */
  shortcodes?: readonly string[] | undefined;
  /** More words the search matches: `["celebrate", "hooray"]`. */
  keywords?: readonly string[] | undefined;
  /**
   * The emoji as plain text, such as its Unicode character (`"๐ŸŽ‰"`), for places an image cannot
   * go (a copied message, a notification). `EmojiText` finds it in messages and draws the image;
   * Ouro never draws it as text.
   */
  text?: string | undefined;
  /** It is the organization's or app's own emoji rather than one of the standard set. Ouro draws it the same; apps can label or treat it apart. */
  custom?: boolean | undefined;
  /**
   * The emoji in Unicode's five skin tones, light to dark; leave it out for an emoji without tones.
   */
  skinTones?: readonly EmojiItemTone[] | undefined;
}

EmojiItemTone

An emoji's image in one skin tone: skinTones on an EmojiItem.

interface EmojiItemTone {
  /** Identifies the emoji in this tone, unique in the catalogue: `"thumbs-up-tone3"`. Reactions and tokens use it. */
  id: string;
  /** The still image's address. */
  src: string;
  /** Sizes of the still image for sharper screens, as `<img srcset>`: `"wave-2x.png 2x"`. */
  srcSet?: string | undefined;
  /** An animated image (GIF, animated WebP or PNG), shown while the emoji plays; the still one shows otherwise. */
  animatedSrc?: string | undefined;
  /** The animation as a filmstrip of square frames, played while the emoji plays, for a tone without `animatedSrc`. */
  filmstrip?: EmojiFilmstrip | undefined;
  /**
   * The emoji in this tone as text (`"๐Ÿ‘๐Ÿฝ"`), which `EmojiText` finds in messages; Ouro never draws it as text.
   */
  text?: string | undefined;
}

Reaction

A reaction on a message, with how many people chose it.

interface Reaction extends ReactionOption {
  /** How many people reacted this way. */
  count: number;
  /** The person using the app is one of them: the reaction is pressed and tinted. */
  mine?: boolean | undefined;
  /**
   * Who reacted, shown in a tooltip on hover and keyboard focus, which screen readers hear as its
   * description: "You and Riley Example". A tooltip never opens on touch, so keep the count
   * meaningful without it. Without it, the tooltip shows the reaction's name.
   */
  tooltip?: string | undefined;
}

ReactionOption

A reaction someone can add: its key, its spoken name and what it shows.

interface ReactionOption {
  /** Identifies the reaction ("like"); `onReact` receives it. */
  key: string;
  /** What screen readers hear for it ("Like"). */
  label: string;
  /** What it shows: an emoji ("๐Ÿ‘") or an icon element. Screen readers hear `label` instead. */
  emoji: ReactNode;
}

Claim sources