Skip to content
txbrendan/ui

Hover Actions

beta

A 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.

icons

danger

disabled

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. 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

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>
  );
}