Components / Buttons
Toolbar A row or column of buttons, toggles and menus that acts as a single tab stop, with arrow-key navigation between controls. Includes groups, separators that follow the orientation, and plain, outline and floating styles.
import {
AlignCenterIcon,
AlignLeftIcon,
AlignRightIcon,
BoldIcon,
ItalicIcon,
LinkIcon,
ListIcon,
ListOrderedIcon,
UnderlineIcon,
} from "lucide-react" ;
import { Button } from "@/components/ui/button" ;
import { ToggleButton } from "@/components/ui/toggle-button" ;
import { ToggleButtonGroup } from "@/components/ui/toggle-button-group" ;
import {
Toolbar,
ToolbarGroup,
ToolbarSeparator,
} from "@/components/ui/toolbar" ;
export default function ToolbarDemo () {
return (
< Toolbar aria-label = "Text formatting" variant = "outline" >
< ToolbarGroup aria-label = "Style" >
< ToggleButton size = "sm" aria-label = "Bold" defaultSelected >
< BoldIcon />
</ ToggleButton >
< ToggleButton size = "sm" aria-label = "Italic" >
< ItalicIcon />
</ ToggleButton >
< ToggleButton size = "sm" aria-label = "Underline" >
< UnderlineIcon />
</ ToggleButton >
</ ToolbarGroup >
< ToolbarSeparator />
< ToggleButtonGroup
aria-label = "Alignment"
variant = "spaced"
size = "sm"
selectionMode = "single"
disallowEmptySelection
defaultSelectedKeys = {[ "left" ]}
className = "gap-0.5"
>
< ToggleButton id = "left" aria-label = "Align left" >
< AlignLeftIcon />
</ ToggleButton >
< ToggleButton id = "center" aria-label = "Align center" >
< AlignCenterIcon />
</ ToggleButton >
< ToggleButton id = "right" aria-label = "Align right" >
< AlignRightIcon />
</ ToggleButton >
</ ToggleButtonGroup >
< ToolbarSeparator />
< ToolbarGroup aria-label = "Insert" >
< Button variant = "ghost" size = "icon-sm" aria-label = "Bulleted list" >
< ListIcon />
</ Button >
< Button variant = "ghost" size = "icon-sm" aria-label = "Numbered list" >
< ListOrderedIcon />
</ Button >
< Button variant = "ghost" size = "icon-sm" aria-label = "Insert link" >
< LinkIcon />
</ Button >
</ ToolbarGroup >
</ Toolbar >
);
}
CLI Manual
$ pnpm dlx shadcn@latest add @desyne/toolbar
The CLI installs dependencies and any other components this one uses.
import { Toolbar, ToolbarGroup, ToolbarSeparator } from "@/components/ui/toolbar" ;
< Toolbar aria-label = "Text formatting" >
< ToolbarGroup aria-label = "Style" >
< ToggleButton aria-label = "Bold" >< BoldIcon /></ ToggleButton >
< ToggleButton aria-label = "Italic" >< ItalicIcon /></ ToggleButton >
</ ToolbarGroup >
< ToolbarSeparator />
< Button variant = "ghost" size = "icon-sm" aria-label = "Insert link" >< LinkIcon /></ Button >
</ Toolbar >
Put any Button , Toggle Button , Toggle Button Group or Menu trigger inside. The toolbar doesn't style its children, so pick ghost buttons and a small size to match.
Toolbar — a set of related commands for one area: editor formatting, canvas tools, bulk actions on selected rows, a document header.
A plain flex row of buttons — two or three unrelated actions such as Cancel and Save. Those should each be tab stops.
Toggle Button Group — a single choice or multi-choice set on its own. Inside a toolbar it becomes one of the toolbar's groups.
Menu — when commands don't fit, move the rest behind a "More" menu trigger in the toolbar.
< Toolbar > { /* role="toolbar" */ }
< ToolbarGroup > { /* role="group" */ }
< Button />
</ ToolbarGroup >
< ToolbarSeparator /> { /* role="separator", perpendicular to the toolbar */ }
< MenuTrigger />
</ Toolbar >
Part Renders Notes Toolbar<div role="toolbar">React Aria Toolbar. Sets aria-orientation and manages arrow-key focus. data-orientation drives the layout. ToolbarGroup<div role="group">Groups related controls. Label it with aria-label. ToolbarSeparator<div role="separator">A vertical line in a horizontal toolbar and a horizontal line in a vertical one, with matching aria-orientation.
orientation="vertical" stacks the controls, for tool palettes on a canvas. Up and down arrows move focus. Separators turn horizontal on their own. Pass orientation="vertical" to a nested ToggleButtonGroup too.
import {
CircleIcon,
HandIcon,
MousePointer2Icon,
PenToolIcon,
SquareIcon,
TypeIcon,
ZoomInIcon,
ZoomOutIcon,
} from "lucide-react" ;
import { useState } from "react" ;
import { Button } from "@/components/ui/button" ;
import { ToggleButton } from "@/components/ui/toggle-button" ;
import { ToggleButtonGroup } from "@/components/ui/toggle-button-group" ;
import {
Toolbar,
ToolbarGroup,
ToolbarSeparator,
} from "@/components/ui/toolbar" ;
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip" ;
const tools = [
{ id: "select" , label: "Select" , key: "V" , icon: MousePointer2Icon },
{ id: "hand" , label: "Hand" , key: "H" , icon: HandIcon },
{ id: "pen" , label: "Pen" , key: "P" , icon: PenToolIcon },
{ id: "rect" , label: "Rectangle" , key: "R" , icon: SquareIcon },
{ id: "ellipse" , label: "Ellipse" , key: "O" , icon: CircleIcon },
{ id: "text" , label: "Text" , key: "T" , icon: TypeIcon },
];
export default function ToolbarVertical () {
const [ tool , setTool ] = useState ( "select" );
const [ zoom , setZoom ] = useState ( 100 );
const active = tools. find (( t ) => t.id === tool);
return (
< div className = "flex items-start gap-6" >
< Toolbar
aria-label = "Canvas tools"
orientation = "vertical"
variant = "floating"
>
< ToggleButtonGroup
aria-label = "Tool"
orientation = "vertical"
variant = "spaced"
size = "md"
selectionMode = "single"
disallowEmptySelection
selectedKeys = {[tool]}
onSelectionChange = {( keys ) => setTool ( String ([ ... keys][ 0 ]))}
className = "gap-0.5"
>
{tools. map (({ id , label , key , icon : Icon }) => (
< TooltipTrigger key = {id} delay = { 400 }>
< ToggleButton id = {id} aria-label = {label}>
< Icon />
</ ToggleButton >
< Tooltip placement = "right" >
{label} < span className = "text-muted-foreground" >{key}</ span >
</ Tooltip >
</ TooltipTrigger >
))}
</ ToggleButtonGroup >
< ToolbarSeparator />
< ToolbarGroup aria-label = "Zoom" >
< Button
variant = "ghost"
size = "icon"
aria-label = "Zoom in"
isDisabled = {zoom >= 400 }
onPress = {() => setZoom (( z ) => Math. min ( 400 , z * 2 ))}
>
< ZoomInIcon />
</ Button >
< Button
variant = "ghost"
size = "icon"
aria-label = "Zoom out"
isDisabled = {zoom <= 25 }
onPress = {() => setZoom (( z ) => Math. max ( 25 , z / 2 ))}
>
< ZoomOutIcon />
</ Button >
</ ToolbarGroup >
</ Toolbar >
< div className = "text-muted-foreground text-sm" aria-live = "polite" >
< p >
Tool:{ " " }
< span className = "font-medium text-foreground" >{active?.label}</ span >
</ p >
< p >
Zoom:{ " " }
< span className = "font-medium text-foreground tabular-nums" >
{zoom}%
</ span >
</ p >
</ div >
</ div >
);
}
plain (default) has no frame, for headers and editor chrome. outline draws a bordered bar on the card surface. floating uses the popover surface and a larger shadow, for bars that hover over content.
import { ClipboardPasteIcon, CopyIcon, ScissorsIcon } from "lucide-react" ;
import { Button } from "@/components/ui/button" ;
import { Toolbar, ToolbarGroup } from "@/components/ui/toolbar" ;
const variants = [ "plain" , "outline" , "floating" ] as const ;
export default function ToolbarVariants () {
return (
< div className = "flex flex-col items-center gap-6" >
{variants. map (( variant ) => (
< div key = {variant} className = "flex items-center gap-4" >
< span className = "w-16 text-muted-foreground text-xs" >{variant}</ span >
< Toolbar aria-label = { `Clipboard (${ variant })` } variant = {variant}>
< ToolbarGroup aria-label = "Clipboard" >
< Button variant = "ghost" size = "sm" >
< ScissorsIcon /> Cut
</ Button >
< Button variant = "ghost" size = "sm" >
< CopyIcon /> Copy
</ Button >
< Button variant = "ghost" size = "sm" >
< ClipboardPasteIcon /> Paste
</ Button >
</ ToolbarGroup >
</ Toolbar >
</ div >
))}
</ div >
);
}
A floating toolbar for actions on selected rows, with a live count. Keep text fields such as search outside the toolbar: the toolbar uses ← and → to move focus, which would stop the cursor moving inside the input.
import {
ArchiveIcon,
DownloadIcon,
FilterIcon,
MailIcon,
Trash2Icon,
XIcon,
} from "lucide-react" ;
import { useState } from "react" ;
import { Badge } from "@/components/ui/badge" ;
import { Button } from "@/components/ui/button" ;
import { SearchField } from "@/components/ui/search-field" ;
import {
Toolbar,
ToolbarGroup,
ToolbarSeparator,
} from "@/components/ui/toolbar" ;
export default function ToolbarTableActions () {
const [ selected , setSelected ] = useState ( 3 );
return (
< div className = "flex w-full max-w-2xl flex-col gap-3" >
< div className = "flex flex-wrap items-center gap-2" >
{ /* Text inputs stay outside the toolbar: it uses ←/→ to move focus. */ }
< SearchField
aria-label = "Search customers"
placeholder = "Search customers"
className = "w-56"
size = "sm"
/>
< Toolbar aria-label = "Customer list" className = "ml-auto" >
< Button variant = "outline" size = "sm" >
< FilterIcon /> Filter
< Badge size = "sm" variant = "soft" >
2
</ Badge >
</ Button >
< Button variant = "ghost" size = "sm" >
< DownloadIcon /> Export
</ Button >
</ Toolbar >
</ div >
{selected > 0 ? (
< Toolbar
aria-label = "Bulk actions"
variant = "floating"
className = "self-center"
>
< span className = "px-2 font-medium text-sm tabular-nums" role = "status" >
{selected} selected
</ span >
< ToolbarSeparator />
< ToolbarGroup aria-label = "Selected customers" >
< Button variant = "ghost" size = "sm" >
< MailIcon /> Email
</ Button >
< Button variant = "ghost" size = "sm" >
< ArchiveIcon /> Archive
</ Button >
< Button variant = "ghost" size = "sm" color = "danger" >
< Trash2Icon /> Delete
</ Button >
</ ToolbarGroup >
< ToolbarSeparator />
< Button
variant = "ghost"
size = "icon-sm"
aria-label = "Clear selection"
onPress = {() => setSelected ( 0 )}
>
< XIcon />
</ Button >
</ Toolbar >
) : (
< Button
variant = "link"
size = "sm"
className = "self-center"
onPress = {() => setSelected ( 3 )}
>
Select 3 customers again
</ Button >
)}
</ div >
);
}
A Menu trigger works like any other button in the toolbar: arrow keys reach it, and Enter , Space or ↓ opens it. Disabled buttons are skipped by arrow keys.
Arrow keys move between controls.
import {
ChevronDownIcon,
ImageIcon,
MinusIcon,
Redo2Icon,
TableIcon,
Undo2Icon,
} from "lucide-react" ;
import { useState } from "react" ;
import { Button } from "@/components/ui/button" ;
import { MenuContent, MenuItem, MenuTrigger } from "@/components/ui/menu" ;
import {
Toolbar,
ToolbarGroup,
ToolbarSeparator,
} from "@/components/ui/toolbar" ;
export default function ToolbarWithMenu () {
const [ last , setLast ] = useState < string | null >( null );
return (
< div className = "flex flex-col items-center gap-3" >
< Toolbar aria-label = "Document" variant = "outline" >
< ToolbarGroup aria-label = "History" >
< Button variant = "ghost" size = "icon-sm" aria-label = "Undo" >
< Undo2Icon />
</ Button >
< Button variant = "ghost" size = "icon-sm" aria-label = "Redo" isDisabled >
< Redo2Icon />
</ Button >
</ ToolbarGroup >
< ToolbarSeparator />
< MenuTrigger >
< Button variant = "ghost" size = "sm" >
Insert < ChevronDownIcon />
</ Button >
< MenuContent
className = "w-44"
onAction = {( key ) => setLast ( String (key))}
>
< MenuItem id = "Image" textValue = "Image" >
< ImageIcon /> Image
</ MenuItem >
< MenuItem id = "Table" textValue = "Table" >
< TableIcon /> Table
</ MenuItem >
< MenuItem id = "Divider" textValue = "Divider" >
< MinusIcon /> Divider
</ MenuItem >
</ MenuContent >
</ MenuTrigger >
< ToolbarSeparator />
< Button size = "sm" >Share</ Button >
</ Toolbar >
< p className = "text-muted-foreground text-xs" aria-live = "polite" >
{last ? `Inserted: ${ last }` : "Arrow keys move between controls." }
</ p >
</ div >
);
}
The toolbar is one tab stop. Tab moves focus into it (to the last focused control) and out of it; arrow keys move between controls.
Every toolbar needs an accessible name: aria-label or aria-labelledby. Label each ToolbarGroup too, so screen readers announce "Alignment, group" when focus enters it.
Icon-only buttons need aria-label. Add a Tooltip with the same text (and the shortcut) for sighted users.
Use ToggleButton for on/off formatting, so the pressed state is announced, and ToggleButtonGroup with selectionMode="single" for mutually exclusive options such as alignment.
In right-to-left locales, ← and → are reversed in a horizontal toolbar.
Key Action Tab / Shift +Tab Moves focus into or out of the toolbar → / ← Next / previous control (horizontal) ↓ / ↑ Next / previous control (vertical) Enter / Space Activates the focused control
Attribute On Present when data-orientationToolbarAlways: horizontal or vertical data-variantToolbarAlways: plain, outline or floating
Slot Element data-slot="toolbar"Root data-slot="toolbar-group"A group data-slot="toolbar-separator"A separator
A horizontal toolbar scrolls sideways when it's wider than its container. To wrap instead, pass className="flex-wrap overflow-visible".
Also accepts every prop of React Aria's Toolbar .
Also accepts every prop of React Aria's Group .
Takes React Aria's Separator props. orientation is set from the toolbar; pass it only to override.
Toggle Button Group — exclusive or multiple toggles, often a toolbar group.
Menu — overflow commands behind a trigger.
Tooltip — labels for icon buttons.
PreviousToggle Button Group Next Form