Sidebar
A column beside the page's content, and a drawer once the window is too narrow to hold one. It renders a real <aside>, which is the complementary landmark.
import { List, ListItem, Sidebar } from 'neba';
<Sidebar label="Sections">
<List variant="text">
<ListItem href="/overview" selected>
Overview
</ListItem>
<ListItem href="/components">Components</ListItem>
</List>
</Sidebar>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | Everything in it: a nav, a filter panel, a table of contents |
| sideshared | 'start' | 'end' | 'start' | Which end of the band it takes. Logical rather than physical, so it flips under RTL. Inside a PageLayout this is already decided by which slot it was handed to |
| width | number | string | size | How wide the column is. Numbers are pixels. With resizable it is only the width the sidebar starts at: dragging writes over it |
| resizable | boolean | false | Lets the reader drag the inner edge to change the column's width. Arrow keys do the same from the keyboard |
| minWidth | number | string | 160 | How narrow it may be dragged |
| maxWidth | number | string | 480 | And how wide |
| onResize | (width: number) => void | — | Fires with the width in pixels while the edge is being dragged |
| onResizeEnd | (width: number) => void | — | Fires once, with the same number, when it is let go |
| collapseBelow | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'none' | PageLayout | The width below which it stops being a column and becomes a drawer. Defaults to the PageLayout's own, and to none outside one: a sidebar that collapsed with nothing able to bring it back is a sidebar the reader has lost |
| open | boolean | — | Whether the drawer is open: only meaningful once it has collapsed. Inside a PageLayout the layout owns this, so control it there |
| defaultOpen | boolean | false | Which state it starts in, for an uncontrolled standalone sidebar |
| onOpenChange | (open: boolean) => void | — | Fires as it opens and closes, whichever of the two owns the state |
| sticky | boolean | true | Whether the column holds its place while the page scrolls past it: a sticky column as tall as what is left of the window under the header |
| title | ReactNode | — | The heading, drawn only while the sidebar is a drawer. A column has the page around it to say what it is; a panel that has covered the page does not |
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the sheet. The panel is never dyed |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The panel's default width and the air around its content |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only: never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| divider | boolean | true | A hairline down the inner edge, the one facing the content. The outer edge is against the window, where there is nothing to be separated from |
| padded | boolean | true | The gutter and the air above and below the content |
| label | string | locale('Sidebar') | The name the region is announced by. A page with two sidebars must have one, or a screen reader offers two regions called "complementary" |
| locale | string | PageLayout | Which language the sidebar's own words are in. Inherited from the PageLayout when there is one |
Every native <aside> attribute passes through, apart from color and title. The shared axes are described under prop conventions.
It lays out its own children and nothing else. To have a page laid out around it, put it in a PageLayout's sidebar or endSidebar slot.
Examples
width · size
size sets the column's default width (md is 16rem), and width overrides it with a number in pixels or any CSS length.
resizable
Lets the reader drag the inner edge. minWidth and maxWidth bound it, onResize fires on every step and onResizeEnd once when the edge is let go: which is where a remembered width should be stored. The handle is a focusable role="separator", so the left and right arrow keys do the same thing.
import { useState } from 'react';
import { List, ListItem, Sidebar, Typography } from 'neba';
export default function SidebarResizable() {
const [width, setWidth] = useState(200);
return (
<div className="flex h-72 w-full overflow-hidden rounded-(--neba-radius-md) border border-(--neba-border)">
<Sidebar
collapseBelow="none"
resizable
width={200}
minWidth={140}
maxWidth={320}
onResize={setWidth}
>
<Typography level="overline">Files</Typography>
<List variant="text" size="sm">
<ListItem href="#">src</ListItem>
<ListItem href="#" selected>
package.json
</ListItem>
<ListItem href="#">README.md</ListItem>
</List>
</Sidebar>
<div className="min-w-0 flex-1 p-4">
<Typography level="h6">Drag the inner edge</Typography>
<Typography color="secondary">{Math.round(width)}px</Typography>
</div>
</div>
);
}side
start and end rather than left and right, because a navigation rail is beside the text it belongs to in every writing direction. Inside a PageLayout the slot decides and the prop is not needed.
import { List, ListItem, Sidebar, Typography } from 'neba';
export default function SidebarSides() {
return (
<div className="flex h-72 w-full overflow-hidden rounded-(--neba-radius-md) border border-(--neba-border)">
<Sidebar collapseBelow="none" width={160} label="Sections">
<Typography level="overline">Sections</Typography>
<List variant="text" size="sm">
<ListItem href="#" selected>
Overview
</ListItem>
<ListItem href="#">Props</ListItem>
</List>
</Sidebar>
<div className="min-w-0 flex-1 p-4">
<Typography level="h6">Two sidebars</Typography>
<Typography color="secondary">Each has its own width and its own drawer.</Typography>
</div>
<Sidebar collapseBelow="none" side="end" width={160} label="On this page" variant="text">
<Typography level="overline">On this page</Typography>
<List variant="text" size="sm">
<ListItem href="#">Props</ListItem>
<ListItem href="#">Examples</ListItem>
</List>
</Sidebar>
</div>
);
}collapseBelow
The width below which the column becomes a Drawer over a scrim, with a focus trap, an Escape and a way back to the trigger. The children exist once either way. title is drawn only in that shape: a column has the page around it to say what it is, a panel that has covered the page does not.
It defaults to the PageLayout's own value and to none outside one, because a sidebar that collapsed with nothing on the page able to bring it back is a sidebar the reader has lost.
sticky
On by default. With the page scrolling it becomes a sticky column as tall as what is left of the window under the header; with only the content scrolling it is already as tall as the layout and this changes nothing.
SidebarTrigger
The button that brings back a sidebar the window has become too narrow to hold. Put it in a Header's brand slot, ahead of the logo.
import { Header, PageLayout, Sidebar, SidebarTrigger } from 'neba';
<PageLayout header={<Header brand={<SidebarTrigger />} />} sidebar={<Sidebar>…</Sidebar>}>
Page
</PageLayout>;| Prop | Type | Default | Description |
|---|---|---|---|
| sideshared | 'start' | 'end' | 'start' | Which of the layout's two sidebars it opens |
| collapseBelow | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'none' | PageLayout | The width below which the button appears: the same one the sidebar collapses at. Inherited from the PageLayout, which is where it should be set |
| icon | ReactNode | 햄버거 | The glyph. Three lines, unless something else is given |
| label | string | locale('Open sidebar') | What it does, in words. Defaults to the locale's "Open sidebar" and "Close sidebar", whichever is true |
| locale | string | PageLayout | Which language that word is in |
Everything else an IconButton takes passes through. It has to be inside a PageLayout to have something to open; outside one it renders nothing. It is hidden at and above the breakpoint by a class rather than by being absent, so it never pops into a header a moment after the page arrives.
Accessibility
- It renders
<aside>, thecomplementarylandmark, and names itself with thelocale's word for "Sidebar" unlesslabelsays otherwise. A page with two sidebars must name both. - Collapsed, it is a modal dialog: the focus is held inside it, Escape closes it, and the focus returns to the trigger.
- The resize handle is a
role="separator"withtabindex="0"and is named by thelocale. Left and right arrows move it by 16px. localeis inherited from the PageLayout, so it is written once per page.