Hover Actions
betaA row's quick icon actions in a small glass toolbar that sits on the row and springs in while the row is hovered or holds focus. A small tooltip above each icon says what it does.
Preview
Point at a row, or Tab to it. The bar springs in at the row's right edge.
- Tom HaleNot replied · Tuesday team sync
- Cal NashMaybe · Tuesday team sync
- Mark ClarkeNot replied · Friday demo day
icons
danger
disabled
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/hover-actions
Use it for
- The quick jobs on a list row, like Reply, Edit, Archive, or Move, so people finish them without leaving the page.
- Keep two to four actions. Put the rest in a Menu.
- Set visible while the row is hovered or holds focus. Focus inside the bar shows it too, so keyboard users always reach it.
- Give each action a shortcut, and use RowActions to make the keys work. The tooltip teaches each one.
- Use icons. Each icon's tooltip says what it does, so the bar stays small and never covers the row.
- Give an action a result when its press changes something, like an archive. The button says what happened, with Undo.
- Place it yourself, usually absolute at the row's right edge, inside a row with position: relative.
Avoid it for
- The only way to do something on touch screens. Touch has no hover, so keep the bar visible there, or offer the action in the row's own page.
- A row's main action, like opening it. Make the row itself the link or button.
- A long bar of actions. Use a Menu for more than four.
- Words in a row's bar, like a text Remind button. They crowd the row. Let the tooltip say the word.
Props
- actions
Required. The actions, in order. Pass the same objects to RowActions and ActionKeys, so the key, the tooltip, and the button never drift apart.
HoverAction[]
- HoverAction
Type. label names the button. shortcut is one key, taught in the tooltip. text shows the label beside the icon. A promise from onClick shows a spinner, and a failure shows Retry. result is ToolButton's ActionResult.
{ label: string; icon?: ReactNode; onClick?: () => void | Promise<unknown>; shortcut?: string; text?: boolean; tone?: "neutral" | "danger"; result?: ActionResult; disabled?: boolean }
- visible
Shows the bar. Set it while the row is hovered or holds focus. Hidden, the bar stays in the Tab order, and focus inside shows it.
boolean · default true
- aria-label
Names the toolbar. Name the row, like 'Actions for Tom Hale', when a list has many.
string · default "Actions"
- ref
Reaches the toolbar. ActionKeys takes it as within.
Ref<HTMLDivElement>
- className
Extra classes on the toolbar. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| actions | HoverAction[] | - | Required. The actions, in order. Pass the same objects to RowActions and ActionKeys, so the key, the tooltip, and the button never drift apart. |
| HoverAction | { label: string; icon?: ReactNode; onClick?: () => void | Promise<unknown>; shortcut?: string; text?: boolean; tone?: "neutral" | "danger"; result?: ActionResult; disabled?: boolean } | - | Type. label names the button. shortcut is one key, taught in the tooltip. text shows the label beside the icon. A promise from onClick shows a spinner, and a failure shows Retry. result is ToolButton's ActionResult. |
| visible | boolean | true | Shows the bar. Set it while the row is hovered or holds focus. Hidden, the bar stays in the Tab order, and focus inside shows it. |
| aria-label | string | "Actions" | Names the toolbar. Name the row, like 'Actions for Tom Hale', when a list has many. |
| ref | Ref<HTMLDivElement> | - | Reaches the toolbar. ActionKeys takes it as within. |
| className | string | - | Extra classes on the toolbar. Later classes win. |
Accessibility
- The bar is a toolbar: one Tab stop, and the arrow keys move between its buttons.
- Each button is named by its action's label. The tooltip shows the label and the key to sighted mouse and keyboard users.
- A hidden bar is only see-through. It stays in the Tab order and the accessibility tree, and focus inside shows it at once.
- A hidden bar ignores the pointer, so a press never lands on a button no one can see.
- Results are said once through a polite live region, and each button's name follows its state.
Examples
On a list row
import { Archive, Pencil, Reply } from "lucide-react";
import { useState } from "react";
function PersonRow({ name }: { name: string }) {
const [lit, setLit] = useState(false);
return (
<div
className="relative flex h-11 items-center px-4"
onMouseEnter={() => setLit(true)}
onMouseLeave={() => setLit(false)}
onFocus={() => setLit(true)}
onBlur={(event) => !event.currentTarget.contains(event.relatedTarget) && setLit(false)}
>
<a href="#">{name}</a>
<HoverActions
visible={lit}
aria-label={`Actions for ${name}`}
className="absolute right-4 top-1/2 -translate-y-1/2"
actions={[
{ icon: <Reply />, label: "Reply", shortcut: "R" },
{ icon: <Pencil />, label: "Edit", shortcut: "E" },
{ icon: <Archive />, label: "Archive", shortcut: "A", result: { label: "Archived", onUndo: () => {} } },
]}
/>
</div>
);
}