Sub Header

Sub Header

A secondary toolbar row below the Header, with left and right control slots for filters, titles, toolbars and view controls.

Playground

leftControls
rightControls
<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.json

Usage

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.

18
<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

PropTypeDefaultDescription
leftControlsReact.ReactNode–Leading slot (data-slot="sub-header-left") – filters, a title or a toolbar. Flex row with an 8px gap.
rightControlsReact.ReactNode–Trailing slot (data-slot="sub-header-right") pinned to the right edge – view controls and overflow actions.
ComponentDescription
SubHeaderSeparatorVertical 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.

Search documentation

Search the component docs