Skip to content

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.

tsx
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

PropTypeDefaultDescription
childrenReactNodeEverything 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
widthnumber | stringsizeHow wide the column is. Numbers are pixels. With resizable it is only the width the sidebar starts at: dragging writes over it
resizablebooleanfalseLets the reader drag the inner edge to change the column's width. Arrow keys do the same from the keyboard
minWidthnumber | string160How narrow it may be dragged
maxWidthnumber | string480And how wide
onResize(width: number) => voidFires with the width in pixels while the edge is being dragged
onResizeEnd(width: number) => voidFires once, with the same number, when it is let go
collapseBelow'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'none'PageLayoutThe 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
openbooleanWhether the drawer is open: only meaningful once it has collapsed. Inside a PageLayout the layout owns this, so control it there
defaultOpenbooleanfalseWhich state it starts in, for an uncontrolled standalone sidebar
onOpenChange(open: boolean) => voidFires as it opens and closes, whichever of the two owns the state
stickybooleantrueWhether 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
titleReactNodeThe 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
elevationshared0 | 1 | 2 | 30Drop shadow depth. 0 means no shadow at all
dividerbooleantrueA 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
paddedbooleantrueThe gutter and the air above and below the content
labelstringlocale('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"
localestringPageLayoutWhich 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.

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.

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.

tsx
import { Header, PageLayout, Sidebar, SidebarTrigger } from 'neba';

<PageLayout header={<Header brand={<SidebarTrigger />} />} sidebar={<Sidebar>…</Sidebar>}>
  Page
</PageLayout>;
PropTypeDefaultDescription
sideshared'start' | 'end''start'Which of the layout's two sidebars it opens
collapseBelow'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'none'PageLayoutThe width below which the button appears: the same one the sidebar collapses at. Inherited from the PageLayout, which is where it should be set
iconReactNode햄버거The glyph. Three lines, unless something else is given
labelstringlocale('Open sidebar')What it does, in words. Defaults to the locale's "Open sidebar" and "Close sidebar", whichever is true
localestringPageLayoutWhich 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>, the complementary landmark, and names itself with the locale's word for "Sidebar" unless label says 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" with tabindex="0" and is named by the locale. Left and right arrows move it by 16px.
  • locale is inherited from the PageLayout, so it is written once per page.

Released under the MIT License