ComponentsCSS
Row actions
The per-item “more” menu for rows, cards and detail panels: icon, label, key hint, dividers and destructive items from one action list.
- Retention review Q3.pdf2.3 MB · Marcus Ode
- Board metrics.xlsx (starred)375 KB · Priya Raman
- Churn interviews.docx94 KB · Amara Okafor
Installation
npx shadcn@latest add https://ui-collections.vercel.app/r/tide/row-actions.jsonAlso installs the theme, cn() and dropdown-menu, button. Packages: clsx, tailwind-merge, radix-ui.
Usage
row-actions-demo.tsx
"use client";
import { useState } from "react";
import { RowActions, type RowAction } from "@/registry/kit/ui/row-actions";
const reports = [
{ id: "r1", name: "Retention review Q3.pdf", meta: "2.3 MB · Marcus Ode" },
{ id: "r2", name: "Board metrics.xlsx", meta: "375 KB · Priya Raman" },
{ id: "r3", name: "Churn interviews.docx", meta: "94 KB · Amara Okafor" },
];
export default function RowActionsDemo() {
const [starred, setStarred] = useState<string[]>(["r2"]);
const [notice, setNotice] = useState("");
const actionsFor = (r: (typeof reports)[number]): RowAction[] => [
{ label: "View details", icon: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-10v5m0-8h.01", onSelect: () => setNotice(`Opened details for ${r.name}.`) },
{
label: starred.includes(r.id) ? "Remove from Starred" : "Add to Starred",
icon: "m12 3.5 2.6 5.3 5.9.9-4.3 4.1 1 5.8-5.2-2.7-5.2 2.7 1-5.8-4.3-4.1 5.9-.9z",
onSelect: () => setStarred((s) => (s.includes(r.id) ? s.filter((x) => x !== r.id) : [...s, r.id])),
},
{ label: "Rename", icon: "M4 20h4L19 9l-4-4L4 16zM13.5 6.5l4 4", shortcut: "F2", separatorBefore: true, onSelect: () => setNotice(`Renaming ${r.name}.`) },
{ label: "Move to folder", icon: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2zM10 13h6M13 10l3 3-3 3", onSelect: () => setNotice(`Choose where ${r.name} goes.`) },
{
label: "Move to Trash",
icon: "M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3",
destructive: true,
separatorBefore: true,
onSelect: () => setNotice(`Moved ${r.name} to Trash.`),
},
];
return (
<div className="grid w-full max-w-md gap-2">
<ul className="surface divide-y divide-line rounded-card">
{reports.map((r) => (
<li key={r.id} className="flex items-center gap-3 py-2.5 pr-2 pl-4">
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">
{r.name}
{starred.includes(r.id) && (
<>
<span aria-hidden className="ml-1.5 text-warning">★</span>
<span className="sr-only"> (starred)</span>
</>
)}
</span>
<span className="block text-xs text-muted">{r.meta}</span>
</span>
<RowActions actions={actionsFor(r)} label={`Actions for “${r.name}”`} />
</li>
))}
</ul>
<p role="status" className="min-h-5 text-sm text-muted">
{notice}
</p>
</div>
);
}API
actions{ label; icon?; onSelect; destructive?; separatorBefore?; disabled?; shortcut? }[]Menu items; icon is an element or a 24×24 SVG path.
label= "More actions"
stringAccessible name of the ⋯ trigger, e.g. Actions for “Q3 report.pdf”.
align= "end"
"start" | "center" | "end"Menu alignment to the trigger.
triggerReactNodeReplaces the ⋯ icon button.
RowActionItemscomponentThe items alone, for a menu you build yourself.
Source
components/ui/row-actions.tsx
"use client";
import { Fragment, type ReactNode } from "react";
import { cn } from "@/registry/kit/lib/utils";
import { Button } from "@/registry/kit/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/registry/kit/ui/dropdown-menu";
export interface RowAction {
/** Verb + object where it helps: "Move to Trash", "Copy link". */
label: string;
/** 24×24 stroke icon, or an SVG path string drawn as one. */
icon?: ReactNode;
onSelect: () => void;
/** Red text, for steps that remove or delete. */
destructive?: boolean;
/** Draws a divider above this item, to group related actions. */
separatorBefore?: boolean;
disabled?: boolean;
/** Key hint on the right, e.g. "F2". */
shortcut?: string;
}
export interface RowActionsProps {
actions: RowAction[];
/** Accessible name of the trigger: "Actions for “Q3 report.pdf”". */
label?: string;
align?: "start" | "center" | "end";
/** Replaces the ⋯ icon button. Must accept a ref (a Button or native element). */
trigger?: ReactNode;
/** Classes for the default ⋯ trigger, e.g. to reveal it on row hover. */
triggerClassName?: string;
onOpenChange?: (open: boolean) => void;
}
const iconFor = (icon: ReactNode) =>
typeof icon === "string" ? (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d={icon} />
</svg>
) : (
icon
);
/**
* The per-item “more” menu for rows, cards and detail panels. Pass the same
* action list everywhere an item appears, so a file offers the same choices
* in a grid, a table and its details sheet.
*/
export function RowActions({ actions, label = "More actions", align = "end", trigger, triggerClassName, onOpenChange }: RowActionsProps) {
if (!actions.length) return null;
return (
<DropdownMenu modal={false} onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild>
{trigger ?? (
<Button size="icon-sm" variant="ghost" aria-label={label} title={label} className={cn("shrink-0", triggerClassName)}>
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden>
<circle cx="5.5" cy="12" r="1.6" />
<circle cx="12" cy="12" r="1.6" />
<circle cx="18.5" cy="12" r="1.6" />
</svg>
</Button>
)}
</DropdownMenuTrigger>
<DropdownMenuContent align={align} className="min-w-48">
<RowActionItems actions={actions} />
</DropdownMenuContent>
</DropdownMenu>
);
}
/** The menu items alone, for a menu you build yourself (a context menu, a split button). */
export function RowActionItems({ actions }: { actions: RowAction[] }) {
return (
<>
{actions.map((action, i) => (
<Fragment key={action.label}>
{action.separatorBefore && i > 0 && <DropdownMenuSeparator />}
<DropdownMenuItem variant={action.destructive ? "danger" : "default"} disabled={action.disabled} onSelect={action.onSelect}>
{action.icon && iconFor(action.icon)}
{action.label}
{action.shortcut && <DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>}
</DropdownMenuItem>
</Fragment>
))}
</>
);
}