Toolbar
A row of commands in one frame. At narrow widths, secondary controls move into a More menu and keep their state.
Loading the live example…
"use client"
import { AlignCenter, AlignLeft, AlignRight, Bold, Italic, Redo2, Underline, Undo2 } from "lucide-react"
import { useState } from "react"
import {
Toolbar,
ToolbarButton,
ToolbarGroup,
ToolbarMenuContent,
ToolbarMenuTrigger,
ToolbarSpacer,
ToolbarToggle,
ToolbarToggleGroup,
} from "@/components/keyline/ui/toolbar"
import { Menu, MenuItem } from "@/components/keyline/ui/menu"
import { Tooltip } from "@/components/keyline/ui/tooltip"
export function MemoEditor() {
const [style, setStyle] = useState<string[]>(["bold"])
const [align, setAlign] = useState<string[]>(["start"])
const [comments, setComments] = useState(true)
const [last, setLast] = useState("none")
return (
<>
<Toolbar aria-label="Memo formatting" overflow="menu">
<ToolbarGroup aria-label="History">
<Tooltip content="Undo" shortcut="⌘Z">
<ToolbarButton icon aria-label="Undo" onClick={() => setLast("Undo")}>
<Undo2 size={16} aria-hidden="true" />
</ToolbarButton>
</Tooltip>
<ToolbarButton icon aria-label="Redo" disabled>
<Redo2 size={16} aria-hidden="true" />
</ToolbarButton>
</ToolbarGroup>
<ToolbarToggleGroup aria-label="Text style" value={style} onValueChange={setStyle}>
<ToolbarToggle icon value="bold" aria-label="Bold">
<Bold size={16} aria-hidden="true" />
</ToolbarToggle>
<ToolbarToggle icon value="italic" aria-label="Italic">
<Italic size={16} aria-hidden="true" />
</ToolbarToggle>
<ToolbarToggle icon value="underline" aria-label="Underline">
<Underline size={16} aria-hidden="true" />
</ToolbarToggle>
</ToolbarToggleGroup>
<ToolbarToggleGroup
aria-label="Alignment"
multiple={false}
value={align}
onValueChange={(v) => v.length && setAlign(v)}
>
<ToolbarToggle icon value="start" aria-label="Align start">
<AlignLeft size={16} aria-hidden="true" />
</ToolbarToggle>
<ToolbarToggle icon value="centre" aria-label="Align centre">
<AlignCenter size={16} aria-hidden="true" />
</ToolbarToggle>
<ToolbarToggle icon value="end" aria-label="Align end">
<AlignRight size={16} aria-hidden="true" />
</ToolbarToggle>
</ToolbarToggleGroup>
<Menu>
<ToolbarMenuTrigger>Insert</ToolbarMenuTrigger>
<ToolbarMenuContent>
<MenuItem onClick={() => setLast("Insert table")}>Table</MenuItem>
<MenuItem onClick={() => setLast("Insert checklist")}>Checklist</MenuItem>
<MenuItem description="From the Scranton letterhead" onClick={() => setLast("Insert signature")}>
Signature block
</MenuItem>
</ToolbarMenuContent>
</Menu>
<ToolbarSpacer />
<ToolbarToggle pressed={comments} onPressedChange={setComments} affix="3">
Comments
</ToolbarToggle>
<ToolbarButton primary onClick={() => setLast("Send")}>
Send memo
</ToolbarButton>
</Toolbar>
<p role="status">
Last command: {last} · style {style.join(", ") || "none"} · align {align[0]} · comments {comments ? "on" : "off"}
</p>
</>
)
}Installation
npx shadcn@latest add @keyline-ui/toolbarRun it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.
Usage
import {
Toolbar,
ToolbarButton,
ToolbarToggle,
} from "@/components/keyline/ui/toolbar"<Toolbar aria-label="Order actions" overflow="menu">
<ToolbarButton primary>Refresh</ToolbarButton>
<ToolbarToggle>Overdue only</ToolbarToggle>
</Toolbar>Examples
In a panel
With layout="cell" the toolbar is the top row of a Panel: a search input, a menu of checkbox and radio items, a separator and a link, on one seam with the content below.
Loading the live example…
"use client"
import { Search } from "lucide-react"
import { useState } from "react"
import {
Toolbar,
ToolbarButton,
ToolbarInput,
ToolbarLink,
ToolbarMenuContent,
ToolbarMenuTrigger,
ToolbarSeparator,
} from "@/components/keyline/ui/toolbar"
import {
Menu,
MenuCheckboxItem,
MenuGroup,
MenuGroupLabel,
MenuRadioGroup,
MenuRadioItem,
MenuSeparator,
} from "@/components/keyline/ui/menu"
import { Panel, PanelBody } from "@/components/keyline/ui/panel"
const LABELS = { client: "Client", rep: "Sales rep", branch: "Branch" }
export function OrdersPanel() {
const [columns, setColumns] = useState({ client: true, rep: true, branch: false })
const [rows, setRows] = useState("comfortable")
const shown = (Object.keys(columns) as (keyof typeof columns)[])
.filter((key) => columns[key])
.map((key) => LABELS[key])
return (
<Panel>
<Toolbar layout="cell" overflow="menu" aria-label="Orders">
<ToolbarInput label="Search orders" placeholder="Search orders" glyph={<Search size={16} aria-hidden="true" />} />
<Menu>
<ToolbarMenuTrigger>Columns</ToolbarMenuTrigger>
<ToolbarMenuContent>
<MenuGroup>
<MenuGroupLabel>Shown</MenuGroupLabel>
<MenuCheckboxItem checked={columns.client} onCheckedChange={(c) => setColumns({ ...columns, client: c })}>
Client
</MenuCheckboxItem>
<MenuCheckboxItem checked={columns.rep} onCheckedChange={(c) => setColumns({ ...columns, rep: c })}>
Sales rep
</MenuCheckboxItem>
<MenuCheckboxItem checked={columns.branch} onCheckedChange={(c) => setColumns({ ...columns, branch: c })}>
Branch
</MenuCheckboxItem>
</MenuGroup>
<MenuSeparator />
<MenuRadioGroup value={rows} onValueChange={setRows}>
<MenuGroupLabel>Rows</MenuGroupLabel>
<MenuRadioItem value="comfortable">Comfortable</MenuRadioItem>
<MenuRadioItem value="compact">Compact</MenuRadioItem>
</MenuRadioGroup>
</ToolbarMenuContent>
</Menu>
<ToolbarSeparator />
<ToolbarButton>Export</ToolbarButton>
<ToolbarLink href="#props">Help</ToolbarLink>
</Toolbar>
<PanelBody>
<p role="status">
Columns: {shown.join(", ") || "none"} · rows {rows}
</p>
</PanelBody>
</Panel>
)
}Vertical
orientation="vertical" makes a tool palette: an exclusive choice of tool, then zoom. Up and Down arrows move between items.
Loading the live example…
"use client"
import { Minus, MousePointer2, Move, Plus, Ruler } from "lucide-react"
import { useState } from "react"
import { Toolbar, ToolbarButton, ToolbarToggle, ToolbarToggleGroup } from "@/components/keyline/ui/toolbar"
export function FloorPlanTools() {
const [tool, setTool] = useState<string[]>(["pointer"])
const [zoom, setZoom] = useState(100)
return (
<div className="flex items-start gap-3">
<Toolbar orientation="vertical" aria-label="Warehouse floor plan tools">
<ToolbarToggleGroup
aria-label="Tool"
multiple={false}
value={tool}
onValueChange={(v) => v.length && setTool(v)}
>
<ToolbarToggle icon value="pointer" aria-label="Select">
<MousePointer2 size={16} aria-hidden="true" />
</ToolbarToggle>
<ToolbarToggle icon value="pan" aria-label="Pan">
<Move size={16} aria-hidden="true" />
</ToolbarToggle>
<ToolbarToggle icon value="measure" aria-label="Measure">
<Ruler size={16} aria-hidden="true" />
</ToolbarToggle>
</ToolbarToggleGroup>
<ToolbarButton icon aria-label="Zoom in" disabled={zoom >= 200} onClick={() => setZoom((z) => z + 25)}>
<Plus size={16} aria-hidden="true" />
</ToolbarButton>
<ToolbarButton icon aria-label="Zoom out" disabled={zoom <= 50} onClick={() => setZoom((z) => z - 25)}>
<Minus size={16} aria-hidden="true" />
</ToolbarButton>
</Toolbar>
<p role="status">
Tool: {tool[0]} · zoom {zoom}%
</p>
</div>
)
}Scroll
The default overflow. Items that don’t fit stay in the row, which scrolls inside its frame and fades the hidden edge. Arrow keys bring the focused item into view.
Loading the live example…
"use client"
import { Toolbar, ToolbarButton, ToolbarSeparator } from "@/components/keyline/ui/toolbar"
export function OrderCommands() {
return (
<Toolbar aria-label="Order commands">
<ToolbarButton>Assign sales rep</ToolbarButton>
<ToolbarButton>Reschedule delivery window</ToolbarButton>
<ToolbarButton affix={<kbd>⌘P</kbd>}>Print invoices</ToolbarButton>
<ToolbarSeparator />
<ToolbarButton>Cancel order</ToolbarButton>
</Toolbar>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
overflow | "scroll" | "menu" | Default "scroll" | menu moves the items that don’t fit into a More menu, with their state. The primary button stays in the row. |
layout | "control" | "cell" | Default "control" | cell drops the toolbar’s own frame, for use inside a TableFrame or Panel. |
overflowLabel | string | Default "More" | Accessible name of the overflow item; its menu says how many items it holds. |
overflowTrigger | "icon" | "label" | Default "icon" | label shows the overflow label as text with a caret, for a row whose items may all move into it. |
orientation | "horizontal" | "vertical" | Default "horizontal" | Direction of the row. |
Items: ToolbarButton, ToolbarToggle, ToolbarToggleGroup, ToolbarSeparator, ToolbarMenu, ToolbarLink, ToolbarInput. Built on Base UI Toolbar.