A frosted macOS toolbar with a title block, icon and labeled buttons, segmented view switches, an expanding search field, separators and spacers, built on Radix Toolbar so the whole bar is one tab stop.
Showing icons. Tab enters the toolbar; arrow keys move between controls.
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight, LayoutGrid, List, Share, Tag } from "lucide-react"
import { Toolbar, ToolbarButton, ToolbarGroup, ToolbarSearch, ToolbarSegment, ToolbarSegmented, ToolbarSeparator, ToolbarSpacer } from "@/components/ballmac/toolbar"
export default function ToolbarDemo() {
const [view, setView] = React.useState("icons")
const [query, setQuery] = React.useState("")
return (
<div className="w-full max-w-2xl overflow-hidden rounded-xl border bg-card shadow-sm">
<Toolbar label="Documents" title="Documents" subtitle="42 items" className="border-b">
<ToolbarGroup>
<ToolbarButton aria-label="Back" icon={<ChevronLeft className="rtl:rotate-180"/>} />
<ToolbarButton aria-label="Forward" icon={<ChevronRight className="rtl:rotate-180"/>} disabled />
</ToolbarGroup>
<ToolbarSegmented type="single" value={view} onValueChange={(v) => v && setView(v)} aria-label="View">
<ToolbarSegment value="icons" aria-label="Icons"><LayoutGrid /></ToolbarSegment>
<ToolbarSegment value="list" aria-label="List"><List /></ToolbarSegment>
</ToolbarSegmented>
<ToolbarSeparator />
<ToolbarButton aria-label="Share" icon={<Share />} />
<ToolbarButton aria-label="Tags" icon={<Tag />} className="max-sm:hidden" />
<ToolbarSpacer />
<ToolbarSearch collapsedWidth="7rem" value={query} onChange={(e) => setQuery(e.target.value)} onClear={() => setQuery("")} />
</Toolbar>
<p className="px-6 py-10 text-center text-sm text-muted-foreground">
Showing <strong className="text-foreground">{view}</strong>
{query ? <> matching “{query}”</> : null}. Tab enters the toolbar; arrow keys move between controls.
</p>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/toolbarInstall the dependencies.
$ pnpm add radix-ui lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/toolbar.tsx// Ballmac UI: Toolbar. https://ui.ballmac.com/components/toolbar "use client" import * as React from "react" import { Search, X } from "lucide-react" import { Toolbar as Primitive } from "radix-ui" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" type ToolbarProps = Omit<React.ComponentProps<typeof Primitive.Root>, "title"> & { /** Accessible name of the toolbar. */ label: string /** Large title at the leading edge. */ title?: React.ReactNode /** Smaller line under the title, such as an item count. */ subtitle?: React.ReactNode } /** * A macOS-style toolbar: translucent, with an optional title block and groups of buttons. * Built on Radix Toolbar, so Left and Right arrows move between buttons and the whole bar is one Tab stop. */ function Toolbar({ label, title, subtitle, className, children, ...props }: ToolbarProps) { return ( <Primitive.Root data-slot="toolbar" aria-label={label} className={cn("flex min-h-[52px] w-full items-center gap-1.5 border-b border-foreground/[0.08] bg-background/70 px-3 text-foreground backdrop-blur-xl backdrop-saturate-150 [scrollbar-width:none] max-sm:overflow-x-auto [&::-webkit-scrollbar]:hidden", className)} {...props} > {(title || subtitle) && ( <div className="me-3 grid min-w-0 shrink-0 leading-tight"> {title && <span className="truncate text-[15px] font-semibold">{title}</span>} {subtitle && <span className="truncate text-[11px] text-muted-foreground">{subtitle}</span>} </div> )} {children} </Primitive.Root> ) } type ToolbarButtonProps = React.ComponentProps<typeof Primitive.Button> & { /** Icon. */ icon?: React.ReactNode /** Show the text label under the icon (the classic macOS toolbar item) instead of icon only. */ labeled?: boolean /** Shown as a pressed (toggled on) button. */ pressed?: boolean } /** An icon button. Icon-only buttons need an aria-label; labeled ones use their text. */ function ToolbarButton({ icon, labeled = false, pressed, className, children, ...props }: ToolbarButtonProps) { return ( <Primitive.Button data-slot="toolbar-button" aria-pressed={pressed} title={props["aria-label"]} className={cn( "inline-flex shrink-0 items-center justify-center rounded-md text-foreground/75 outline-none transition-colors duration-100 hover:bg-foreground/[0.07] hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-40 aria-pressed:bg-foreground/[0.1] aria-pressed:text-foreground [&_svg]:size-[18px] [&_svg]:shrink-0", labeled ? "min-w-14 flex-col gap-0.5 px-2 py-1 text-[10px]" : "h-8 min-w-8 gap-1.5 px-1.5 text-[13px]", className )} {...props} > {icon} {children} </Primitive.Button> ) } function ToolbarGroup({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="toolbar-group" role="group" className={cn("flex items-center gap-0.5", className)} {...props} /> } type ToolbarSegmentedProps = React.ComponentProps<typeof Primitive.ToggleGroup> /** A single-choice segmented control, such as icon / list / column view. Use ToolbarSegment for the choices. */ function ToolbarSegmented({ className, ...props }: ToolbarSegmentedProps) { return ( <Primitive.ToggleGroup data-slot="toolbar-segmented" className={cn("inline-flex items-center gap-px rounded-lg bg-foreground/[0.06] p-0.5", className)} {...props} /> ) } function ToolbarSegment({ className, ...props }: React.ComponentProps<typeof Primitive.ToggleItem>) { return ( <Primitive.ToggleItem data-slot="toolbar-segment" className={cn( "inline-flex h-6 min-w-8 items-center justify-center rounded-md px-1.5 text-foreground/70 outline-none transition-[background-color,color,box-shadow] duration-100 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[state=on]:bg-background data-[state=on]:text-foreground data-[state=on]:shadow-[0_0.5px_1.5px_rgb(0_0_0/0.25)] dark:data-[state=on]:bg-foreground/[0.18] [&_svg]:size-4", className )} {...props} /> ) } function ToolbarSeparator({ className, ...props }: React.ComponentProps<typeof Primitive.Separator>) { return <Primitive.Separator data-slot="toolbar-separator" className={cn("mx-1.5 h-5 w-px bg-foreground/15", className)} {...props} /> } /** Pushes everything after it to the trailing edge. */ function ToolbarSpacer({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="toolbar-spacer" aria-hidden="true" className={cn("flex-1", className)} {...props} /> } type ToolbarSearchProps = Omit<React.ComponentProps<"input">, "type"> & { /** Width when collapsed, as a CSS length. */ collapsedWidth?: string /** Width when focused or filled. */ expandedWidth?: string /** Called when the clear button is pressed. */ onClear?: () => void } /** A search field that widens when focused, with a clear button. */ function ToolbarSearch({ collapsedWidth = "9rem", expandedWidth = "15rem", className, value, onClear, "aria-label": ariaLabel, ...props }: ToolbarSearchProps) { const msg = useMessages() ariaLabel ??= msg("toolbar.ariaLabel", "Search") const filled = typeof value === "string" ? value.length > 0 : false return ( <div data-slot="toolbar-search" className="group/search relative w-(--collapsed) shrink-0 transition-[width] duration-200 ease-out focus-within:w-(--expanded) motion-reduce:transition-none" style={{ ["--collapsed" as string]: collapsedWidth, ["--expanded" as string]: expandedWidth }} > <Search aria-hidden="true" className="pointer-events-none absolute top-1/2 start-2 size-3.5 -translate-y-1/2 text-muted-foreground" /> <input type="search" value={value} aria-label={ariaLabel} placeholder={msg("toolbar.search", "Search")} className={cn( "h-7 w-full rounded-md border border-foreground/10 bg-foreground/[0.05] pe-7 ps-7 text-[13px] outline-none transition-[box-shadow,background-color] placeholder:text-muted-foreground focus:bg-background focus-visible:ring-[3px] focus-visible:ring-ring/50 [&::-webkit-search-cancel-button]:hidden", className )} {...props} /> {filled && onClear && ( <button type="button" aria-label={msg("toolbar.clearSearch", "Clear search")} onClick={onClear} className="absolute top-1/2 end-1.5 flex size-4 -translate-y-1/2 items-center justify-center rounded-full bg-foreground/30 text-background outline-none hover:bg-foreground/50 focus-visible:ring-2 focus-visible:ring-ring" > <X aria-hidden="true" className="size-2.5" strokeWidth={3} /> </button> )} </div> ) } export { Toolbar, ToolbarButton, ToolbarGroup, ToolbarSegmented, ToolbarSegment, ToolbarSeparator, ToolbarSpacer, ToolbarSearch, type ToolbarProps, type ToolbarButtonProps, type ToolbarSearchProps, }Update the import paths to match your project setup.
Usage
import { Toolbar, ToolbarButton, ToolbarGroup, ToolbarSegmented, ToolbarSegment, ToolbarSeparator, ToolbarSpacer, ToolbarSearch } from "@/components/ballmac/toolbar"The full example is in the Code tab above.
Examples
Labeled buttons
import { Download, FolderPlus, Pencil, Trash2 } from "lucide-react"
import { Toolbar, ToolbarButton, ToolbarGroup, ToolbarSeparator, ToolbarSpacer } from "@/components/ballmac/toolbar"
export default function ToolbarLabeled() {
return (
<div className="w-full max-w-2xl overflow-hidden rounded-xl border bg-card">
<Toolbar label="Actions">
<ToolbarGroup>
<ToolbarButton labeled icon={<FolderPlus />}>New Folder</ToolbarButton>
<ToolbarButton labeled icon={<Pencil />}>Rename</ToolbarButton>
</ToolbarGroup>
<ToolbarSeparator />
<ToolbarButton labeled icon={<Download />}>Download</ToolbarButton>
<ToolbarSpacer />
<ToolbarButton labeled icon={<Trash2 />}>Delete</ToolbarButton>
</Toolbar>
</div>
)
}API reference
<Toolbar>
| Prop | Type | Default |
|---|---|---|
label*Accessible name of the toolbar. | string | — |
titleLarge title at the leading edge. | React.ReactNode | — |
subtitleSmaller line under the title, such as an item count. | React.ReactNode | — |
<ToolbarButton>
| Prop | Type | Default |
|---|---|---|
iconIcon. | React.ReactNode | — |
labeledShow the text label under the icon (the classic macOS toolbar item) instead of icon only. | boolean | false |
pressedShown as a pressed (toggled on) button. | boolean | — |
<ToolbarSearch>
| Prop | Type | Default |
|---|---|---|
collapsedWidthWidth when collapsed, as a CSS length. | string | "9rem" |
expandedWidthWidth when focused or filled. | string | "15rem" |
onClearCalled when the clear button is pressed. | () => void | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab | Enters and leaves the toolbar as a single stop |
| ArrowLeft / ArrowRight | Moves between buttons; Home and End jump to the ends |
| Screen readers | Named by its label; pressed buttons use aria-pressed |
Use with AI
<Toolbar label title subtitle> with ToolbarButton (icon, labeled, pressed), ToolbarGroup, ToolbarSegmented + ToolbarSegment (single choice), ToolbarSeparator, ToolbarSpacer and ToolbarSearch. Arrow keys move between buttons. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Toolbar (@ballmac/toolbar) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Document and file-manager windows
- Editors with many commands
Not for
- Page navigation (navbar)
- One or two buttons (button-group)
Registry JSON: https://ui.ballmac.com/r/toolbar.json
Credits
Free to use in personal and commercial projects.
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Mac Window
macOS window chrome: traffic lights whose glyphs appear on hover, a centered title or unified toolbar, an optional translucent sidebar with vibrancy, and active and inactive appearances.
Finder Window
A working mini Finder: sidebar shortcuts, back and forward history, icon and list views, search, click, shift and command selection, keyboard navigation and a path bar, on a macOS window.
Button Group
Join adjacent actions into one compact control surface while retaining each button's visible keyboard focus and accessible name.
Control Center
The macOS Control Center panel in frosted glass: toggle tiles with round badges, grouped Wi-Fi, Bluetooth and AirDrop rows, pill sliders with the icon in the track, and a Now Playing tile. Real toggle buttons and Radix sliders.