Toolbar
A row of icon buttons in groups with hairlines, tooltips and arrow key focus.
npx shadcn@latest add https://ui.goodtabs.co/r/toolbar.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/toolbar
Text formatting in three groups. Tab in once, then use the arrow keys.
With pressed state
Toggles that report aria-pressed. Bold starts on.
Compact size
The sm size with 28px buttons, one of them disabled.
Usage
import { Toolbar, ToolbarButton, ToolbarGroup, ToolbarSeparator } from "@/components/ui/toolbar"
import { BoldIcon, ItalicIcon, LinkIcon } from "lucide-react"
<Toolbar label="Formatting">
<ToolbarGroup aria-label="Style">
<ToolbarButton label="Bold" pressed={bold} onClick={toggleBold}>
<BoldIcon />
</ToolbarButton>
<ToolbarButton label="Italic">
<ItalicIcon />
</ToolbarButton>
</ToolbarGroup>
<ToolbarSeparator />
<ToolbarButton label="Add link">
<LinkIcon />
</ToolbarButton>
</Toolbar>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | Accessible name. On Toolbar it names the toolbar; on ToolbarButton it is the aria-label and tooltip text. Required on both. | |
| size | "sm" | "default" | "default" | Button size on Toolbar: 28px or 32px. Every ToolbarButton inside follows it. |
| pressed | boolean | On ToolbarButton: marks a toggle as on, with aria-pressed and the primary fill. | |
| children | ReactNode | On ToolbarButton: the icon. On Toolbar: groups, buttons and separators. |
Source
toolbar/toolbar.tsx
"use client";
import * as React from "react";
import { Toolbar as ToolbarPrimitive } from "radix-ui";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
type Size = "sm" | "default";
const SizeContext = React.createContext<Size>("default");
type ToolbarProps = Omit<React.ComponentProps<typeof ToolbarPrimitive.Root>, "aria-label" | "orientation"> & {
/** Accessible name of the toolbar, for example "Formatting". */
label: string;
/** Button size: 28px or 32px. */
size?: Size;
};
/**
* A row of icon buttons in groups divided by hairlines. One tab stop: arrow keys move
* between buttons, Home and End jump to the ends. Compose it from ToolbarGroup, ToolbarButton and ToolbarSeparator.
*/
function Toolbar({ label, size = "default", className, ...props }: ToolbarProps) {
return (
<SizeContext.Provider value={size}>
<TooltipProvider>
<ToolbarPrimitive.Root
aria-label={label}
orientation="horizontal"
data-slot="toolbar"
data-size={size}
className={cn("inline-flex max-w-full items-center gap-px rounded-lg border bg-background p-1", className)}
{...props}
/>
</TooltipProvider>
</SizeContext.Provider>
);
}
/** A set of related buttons. Pass aria-label to name the group. */
function ToolbarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div role="group" data-slot="toolbar-group" className={cn("flex items-center gap-px", className)} {...props} />
);
}
type ToolbarButtonProps = Omit<
React.ComponentProps<typeof Button>,
"aria-label" | "aria-pressed" | "variant" | "size"
> & {
/** Accessible name. Becomes the aria-label and the tooltip text. */
label: string;
/** Marks a toggle as on, with aria-pressed. */
pressed?: boolean;
};
/** A ghost icon button with a tooltip. The icon goes in children. */
function ToolbarButton({ label, pressed, type = "button", className, children, ...props }: ToolbarButtonProps) {
const size = React.useContext(SizeContext);
return (
<Tooltip>
<TooltipTrigger asChild>
<ToolbarPrimitive.Button asChild>
<Button
type={type}
variant="ghost"
size={size === "sm" ? "icon-sm" : "icon"}
aria-label={label}
aria-pressed={pressed}
data-pressed={pressed || undefined}
data-slot="toolbar-button"
className={cn(
"rounded-sm",
pressed && "bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground",
className,
)}
{...props}
>
{children}
</Button>
</ToolbarPrimitive.Button>
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
);
}
/** A short vertical hairline between groups. */
function ToolbarSeparator({ className, ...props }: Omit<React.ComponentProps<typeof Separator>, "orientation">) {
return (
<Separator
orientation="vertical"
data-slot="toolbar-separator"
className={cn("mx-1 data-vertical:h-4 data-vertical:self-center", className)}
{...props}
/>
);
}
export { Toolbar, ToolbarGroup, ToolbarButton, ToolbarSeparator };
For AI agents
Hand your agent this page, the markdown at /components/toolbar/prompt.md, or the Copy AI prompt button above.
When to use
- Editor and canvas controls: formatting, alignment, zoom, history
- A floating action row above a selection or at the top of a panel
- Any set of five or more icon actions that belong together and should be one tab stop
When not to use
- For two or three standalone actions; use icon buttons on their own
- For mutually exclusive choices with a visible selection; use a toggle group or segmented tabs
- For navigation between pages; use tabs or a navbar
Agent notes
- It is radix Toolbar underneath: one tab stop, arrow keys move between buttons, Home and End jump, disabled buttons are skipped
- ToolbarButton is always ghost and sized by the Toolbar; pass onClick from a client component and pressed for toggles
- Toolbar wraps its own TooltipProvider, so tooltips need no setup; separators are decorative
- The frame is rounded-lg with a hairline and 4px padding; pass className="border-0 p-0" to drop it inside another bar