Actions
The quiet icon-action row under assistant replies — type-enforced accessible names, per-action tooltips, and an opt-in reveal-on-hover inside Message.
<Actions> <TooltipProvider> <Tooltip> <TooltipTrigger asChild> <Clipboard value={reply} aria-label="Copy message" variant="ghost" size="xs" className="text-fg-muted hover:text-fg-default" /> </TooltipTrigger> <TooltipContent>Copy message</TooltipContent> </Tooltip> </TooltipProvider> <Action tooltip="Regenerate response"> <ArrowClockwise /> </Action> <Action tooltip="Good response"> <ThumbsUp /> </Action> <Action tooltip="Poor response"> <ThumbsDown /> </Action></Actions>Usage
import { Action, Actions, actionsRevealOnHoverClasses } from "@reva/ui";Actions is the row of icon-only controls under an assistant reply — copy, regenerate, vote. The container bakes in no specific actions; the consumer composes Actions. Each one is the house IconButton (ghost / xs) with muted ink that sharpens on hover, so the row reads as quiet chrome under the message rather than a toolbar. Children are the Phosphor icon.
An Action is icon-only, so it must carry an accessible name — enforced at the type level: pass tooltip (which titles the control and doubles as the default aria-label — a tooltip describes, it does not name) or an explicit aria-label. The props union makes "neither" a type error.
Each tooltipped Action wraps its own TooltipProvider: the house Tooltip requires a provider ancestor (the Radix provider context has no default), so the per-action provider makes the bar zero-setup — and apps with an app-level provider just get the inner one's house-default instant delay.
The copy control composes as a sibling. The house Clipboard renders its own icon-button-shaped control, so a copy action sits as a sibling of Actions — never nested inside one, which would nest buttons:
<Clipboard
value={text}
aria-label="Copy message"
variant="ghost"
size="xs"
className="text-fg-muted hover:text-fg-default"
/>The className matches Action's quiet ink — a bare ghost renders text-neutral-fg-default, which would read warmer than its neighbours. Clipboard has no tooltip of its own; wrap it in the house Tooltip if the row's other actions are tooltipped (as the hero does).
Disabled actions can't show their tooltip
Button's disabled styling uses pointer-events-none, so a disabled Action never receives the hover that would open its tooltip — the accessible name survives, the visual hint doesn't. If users need to learn why a control is off, surface that outside the tooltip.
Composition
Use the following composition to build an Actions bar:
Actions (the quiet bar — flex row, 4px gap)
├── Clipboard (copy — a sibling control, Tooltip-wrapped to match the row)
└── Action (× n) (IconButton ghost/xs — tooltip or aria-label required)Examples
Reveal on message hover
actionsRevealOnHoverClasses is the opt-in: inside a Message — whose root carries the named group the classes pair with — the bar rests hidden and reveals on row hover, or when focus lands anywhere inside the message, so keyboard users tabbing onto the actions reveal them too. Two always-visible escapes: touch devices (no hover to reveal with) and reduced motion.
It is deliberately not the default: outside a Message there is no group ancestor, and a baked-in opacity-0 would leave a standalone bar permanently invisible.
<Message from="assistant"> <MessageContent>{reply}</MessageContent> <Actions className={actionsRevealOnHoverClasses}>…</Actions></Message>Accessibility
- Accessible names are enforced at the type level —
tooltiporaria-label, "neither" doesn't compile — and the tooltip string doubles as the default name so the two never drift apart. - The reveal opt-in is keyboard- and touch-safe: focus anywhere inside the message reveals the bar, and touch or reduced-motion users see it always.
- The reveal is a pure CSS opacity transition, kept instant under reduced motion.
Props
| Component | Prop | Type | Default | Description |
|---|---|---|---|---|
Actions | — | div props | — | The bar — a flex row with a 4px gap; compose Actions and the copy control inside. |
Action | tooltip | string | — | Titles the control and doubles as the default accessible name. One of tooltip / aria-label is required (type-enforced). |
Action | aria-label | string | the tooltip string | Explicit accessible name — required when tooltip is omitted. |
Action | variant / size | IconButton variants | "ghost" / "xs" | Pass through to the underlying IconButton. |
Action | children | ReactNode | — | The Phosphor icon. |
actionsRevealOnHoverClasses is exported alongside the components — a class string passed via className to opt an in-Message bar into the reveal behaviour.
Code Block
The single highlighted-code surface — lazy shiki that costs nothing until code renders, line numbers, an enforced copy control, optional download, and a plain-text fallback for unknown languages.
Sources
Retrieval citations two ways — the Used-N-sources disclosure under a reply, and superscript inline markers whose hovercard peeks the source.