Toolbar
Lays controls out in a bar. Use it for an application header, a page's action row, or the status strip under an editor.
import { Toolbar } from 'neba';
<Toolbar render={<header />} start={<Logo />} end={<Button>Deploy</Button>}>
Workspace
</Toolbar>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the bar, said the way a container says it: the sheet is never dyed, because a toolbar holds other people’s controls and those arrive with colours of their own |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The scale of the padding and the radius. Not a height: a toolbar is as tall as the controls in it plus its padding |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only: which is why there is no separate `dense` prop meaning the same thing |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. `0` even when the bar is pinned: a shadow under a header says "there is content beneath this", and that is only true once the page has been scrolled |
| positionshared | 'static' | 'sticky' | 'fixed' | 'static' | How the bar sits in the page’s scroll. `sticky` takes up its own space and stops at the edge, so nothing underneath has to be padded around it; `fixed` leaves the flow entirely, so the page needs padding of its own |
| sideshared | 'top' | 'bottom' | 'top' | Which edge it is held against when `position` is not `static` |
| divider | boolean | false | Draws a hairline along the edge that faces the content: under a `top` bar, over a `bottom` one |
| start | ReactNode | — | Pinned to the start of the bar: a logo, a title, a back button |
| end | ReactNode | — | Pinned to the end: the actions |
| render | useRender.RenderProp | — | Renders something other than a div (render={<header />}). Base UI's own escape hatch |
| children | ReactNode | — | The middle. Takes whatever width `start` and `end` leave |
Every native <div> attribute passes through.
There are three slots: start and end are pinned to their ends and children takes what is left, so no spacer element is needed to push things apart.
Examples
size and density
A Toolbar has no height of its own. It is as tall as the controls in it plus its padding, and size and density set that padding. density="compact" gives a dense bar without moving the type scale.
import { Button, ButtonGroup, TextField, Toolbar, Typography } from 'neba';
const DENSITIES = ['default', 'compact'] as const;
/**
* A Toolbar has no height of its own. It is as tall as the controls in it plus
* its padding — and `density` is that padding, which is the only thing it is
* ever allowed to change.
*/
export default function ToolbarDensity() {
return (
<div className="flex w-full flex-col gap-5">
{DENSITIES.map((density) => (
<div key={density} className="flex flex-col gap-2">
<Typography level="caption">density="{density}"</Typography>
<Toolbar
density={density}
className="w-full"
start={<Typography level="h6">Files</Typography>}
end={
<ButtonGroup variant="outline" size="sm" color="secondary">
<Button>Rename</Button>
<Button>Move</Button>
</ButtonGroup>
}
>
<TextField size="sm" placeholder="Filter…" fullWidth />
</Toolbar>
</div>
))}
</div>
);
}position and side
position is CSS's own three values, spelled the way CSS spells them.
sticky: what an application header usually wants. It takes up its own space, so nothing underneath needs padding around it.fixed: leaves the flow, so the page needs padding of its own or the first screenful sits behind the bar.
side is the edge the bar pins to. A pinned bar drops its radius, because a rounded corner against the edge of the screen is a gap with nothing behind it.
import { Button, Toolbar, Typography } from 'neba';
/**
* `position="sticky"` is what an application header usually wants: it takes up
* its own space, so nothing underneath has to be padded around it. `divider`
* gives it the rule that says there is content beneath.
*
* The box below scrolls; the bar does not.
*/
export default function ToolbarSticky() {
return (
<div className="h-64 w-full overflow-y-auto rounded-(--neba-radius-md) [border:1px_solid_var(--neba-border)]">
<Toolbar
position="sticky"
divider
variant="solid"
density="compact"
start={<Typography level="h6">Changelog</Typography>}
end={
<Button size="sm" variant="text">
Subscribe
</Button>
}
/>
<div className="flex flex-col gap-3 p-4">
{Array.from({ length: 12 }, (_, index) => (
<Typography key={index} level="body">
{`1.0.${12 - index} — a release note long enough to make the page scroll under the bar.`}
</Typography>
))}
</div>
</div>
);
}divider and elevation
divider rules under the bar to show there is content beneath. elevation stays 0 even when the bar is pinned, so either raise it yourself on scroll or leave it flat and turn on divider.
color
color reaches the rule and the focus ring. A bar that holds other people's controls does not fill its own sheet.
Accessibility
- It does not set
role="toolbar". That role promises one tab stop for the bar with arrow keys inside it, and claiming it without implementing it is worse for a keyboard user than not claiming it. - For a page header, pass
render={<header />}: a real landmark, which is what a screen reader user navigates by. - For a group of controls navigated with the arrow keys, use ButtonGroup.