Sub Header
A secondary toolbar row below the Header, with left and right control slots for filters, titles, toolbars and view controls.
Playground
<SubHeader
leftControls={
<>
<Select items={leadOwnerItems} defaultValue="alicia">
<SelectTrigger variant="subtle" size="sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{leadOwnerItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<Select items={statusItems} defaultValue="open">
<SelectTrigger variant="subtle" size="sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{statusItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</>
}
rightControls={
<>
<Button variant="secondary" size="sm">
<ListFilter data-icon="inline-start" /> Filter
</Button>
<Button variant="secondary" size="sm">
<ArrowUpDown data-icon="inline-start" /> Sort
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="secondary" size="icon-sm" aria-label="More" />
}
>
<Ellipsis />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Import</DropdownMenuItem>
<DropdownMenuItem>Export</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
}
/>Preview
Pass toolbar content to the leftControls and rightControls slots – each renders as a horizontal group with consistent spacing, and the right slot pins to the trailing edge. Unlike Header, there is no bottom border by default.
<SubHeader
leftControls={
<>
<Select items={leadOwnerItems} defaultValue="alicia">
<SelectTrigger variant="subtle" size="sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{leadOwnerItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<Select items={statusItems} defaultValue="open">
<SelectTrigger variant="subtle" size="sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{statusItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</>
}
rightControls={
<>
<Button variant="secondary" size="sm">
<ListFilter data-icon="inline-start" /> Filter
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="secondary" size="icon-sm" aria-label="More" />
}
>
<Ellipsis />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Import</DropdownMenuItem>
<DropdownMenuItem>Export</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
}
/>Installation
npx shadcn@latest add https://espresso-base-ui.vercel.app/r/sub-header.jsonUsage
import { SubHeader, SubHeaderSeparator } from "@/components/ui/sub-header"<SubHeader
leftControls={<>{/* filters, title, toolbar */}</>}
rightControls={<>{/* view controls, actions */}</>}
/>Toolbar with separators
Group related controls in the left slot and divide the groups with SubHeaderSeparator – a vertical hairline that matches the toolbar height.
<SubHeader
leftControls={
<>
<Select items={fontItems} defaultValue="inter">
<SelectTrigger variant="ghost" size="sm">
<SelectValue />
</SelectTrigger>
<SelectContent>{/* font items */}</SelectContent>
</Select>
<Button variant="ghost" size="icon-sm" aria-label="Decrease size">
<Minus />
</Button>
<span className="text-sm text-secondary-foreground">18</span>
<Button variant="ghost" size="icon-sm" aria-label="Increase size">
<Plus />
</Button>
<SubHeaderSeparator />
<Button variant="ghost" size="icon-sm" aria-label="Bold">
<Bold />
</Button>
<Button variant="ghost" size="icon-sm" aria-label="Italic">
<Italic />
</Button>
<Button variant="ghost" size="icon-sm" aria-label="Underline">
<Underline />
</Button>
</>
}
/>View controls
The right slot pins view-level actions – column visibility, grouping, filtering and sorting – to the trailing edge while filters stay on the left.
<SubHeader
leftControls={
<Select items={organizationItems} defaultValue="acme">
<SelectTrigger variant="subtle" size="sm">
<SelectValue />
</SelectTrigger>
<SelectContent>{/* organization items */}</SelectContent>
</Select>
}
rightControls={
<>
<Button variant="secondary" size="sm">
<EyeOff data-icon="inline-start" /> Columns
</Button>
<Button variant="secondary" size="sm">
<ListFilter data-icon="inline-start" /> Filter
</Button>
<Button variant="secondary" size="sm">
<ArrowUpDown data-icon="inline-start" /> Sort
</Button>
</>
}
/>API reference
SubHeader renders a div and accepts all div props; slots render only when provided. Overflowing toolbars can scroll by adding overflow-x-auto plus shrink-0 on the slot children via className.
SubHeader
| Prop | Type | Default | Description |
|---|---|---|---|
| leftControls | React.ReactNode | – | Leading slot (data-slot="sub-header-left") – filters, a title or a toolbar. Flex row with an 8px gap. |
| rightControls | React.ReactNode | – | Trailing slot (data-slot="sub-header-right") pinned to the right edge – view controls and overflow actions. |
| Component | Description |
|---|---|
| SubHeaderSeparator | Vertical hairline divider between control groups (data-slot="sub-header-separator"). 20px tall, role="separator". |
Accessibility & styling hooks
The root exposes data-slot="sub-header" and the slots data-slot="sub-header-left" / data-slot="sub-header-right" for CSS targeting – the dashboards use these to make crowded toolbars horizontally scrollable. The separator renders role="separator" with aria-orientation="vertical"; give icon-only buttons an aria-label.