Skip to content
TopClass UI

Sidebar

A rail of server-rendered links that collapses to a column of icons. Only the rail and its toggle run in the browser.

split - size not measured yet

Sidebar

split - size not measured yet

A rail of server-rendered links that collapses to a column of icons. Only the rail and its toggle run in the browser.

Install

Try it

defaultOpen
open
current

The code

sidebar.tsx
import { folder } from 'topclassui/icons/folder'
import { gear } from 'topclassui/icons/gear'
import { house } from 'topclassui/icons/house'
import { Svg } from 'topclassui/svg'
import { SidebarBody, SidebarGroup, SidebarHeader, SidebarLink, SidebarRoot } from 'topclassui/sidebar'

<div className="h-96">
  <SidebarRoot>
    <SidebarHeader>Workspace</SidebarHeader>
    <SidebarBody>
      <SidebarGroup>
        <SidebarLink href="#home" icon={<Svg icon={house} />}>Home</SidebarLink>
        <SidebarLink href="#projects" icon={<Svg icon={folder} />}>Projects</SidebarLink>
        <SidebarLink href="#settings" icon={<Svg icon={gear} />}>Settings</SidebarLink>
      </SidebarGroup>
    </SidebarBody>
  </SidebarRoot>
</div>

defaultOpen={false} sends the rail collapsed in the server HTML, so nothing moves when the page hydrates.

Every state

2 states

Import from the package

topclassui/sidebar
import { SidebarRoot, SidebarTrigger, SidebarHeader, SidebarBody, SidebarGroup, SidebarLink } from 'topclassui/sidebar'

Install once:npm i topclassui@beta zod

Examples

  • Six flat parts and no bare Sidebar. SidebarRoot and SidebarTrigger run in the browser; SidebarHeader, SidebarBody, SidebarGroup and SidebarLink are server HTML. SidebarHeader draws the toggle itself.
  • Give the parent of the rail a height, such as h-dvh. A long list then scrolls inside SidebarBody, and the page does not.
  • In a flex row the rail keeps its 220 px, or 60 collapsed. Give the column beside it min-w-0 flex-1.
  • A link takes href, or render={<Link href="/docs" />} in Next.js. Every other prop goes to the link, not to the list item around it.
  • Give every link an icon when the rail can collapse: collapsed, the icon is all that is drawn.
  • open with onOpenChange needs a 'use client' file, as the controlled example shows. A button outside the rail can then toggle it.
  • Another width: className="data-open:w-72" on SidebarRoot; the collapsed width stays. On a phone, start it collapsed: the drawer form is not built yet.
basic
groups
collapsed
controlled

The rail asked to change 0 times.

in-a-layout

Orders

Orders
OrderCustomerPlacedStatusTotal
ORD-5521Northwind Traders12 Mar 2026Awaiting payment$1,250.00
ORD-5520Contoso Ltd11 Mar 2026Shipped$480.00
ORD-5519Fabrikam Inc11 Mar 2026Awaiting payment$9,120.50
long-list
In Arabic, right to left

Props

SidebarRoot

The props of SidebarRoot
PropTypeDefaultLimitsDescription
childrenReactNode--A SidebarHeader, then a SidebarBody holding the groups and their links.
defaultOpenbooleantrue-SidebarRoot: whether the rail starts expanded. false starts it as a column of icons, in the server HTML too.
onOpenChange((open: boolean) => void)--Called with the state the trigger asks for, once a press. A function: pass it from a client file.
openboolean--SidebarRoot: the state, held by the caller. Pair it with onOpenChange, in a client file.

SidebarTrigger

No props of its own: it takes the attributes of the element it renders.

SidebarHeader

The props of SidebarHeader
PropTypeDefaultLimitsDescription
childrenReactNode--The title beside the trigger. It is hidden, and still read out, while the rail is collapsed.

SidebarBody

The props of SidebarBody
PropTypeDefaultLimitsDescription
childrenReactNode--The groups. A list longer than the rail scrolls here, under the header.

SidebarGroup

The props of SidebarGroup
PropTypeDefaultLimitsDescription
childrenReactNode--The SidebarLink rows of the group.
labelReactNode--The heading of the group, which names its list. Hidden, and still read out, while the rail is collapsed.

SidebarLink

The props of SidebarLink
PropTypeDefaultLimitsDescription
aria-labelstring--
childrenReactNode--The label. It ends in an ellipsis when the rail is too narrow, and is cut at 100 characters.
hrefstring--Where the row goes.
renderReactElement--The link as another element, such as render={<Link href="/docs" />}, which carries its own href.
currentbooleanfalse-SidebarLink: the page being read. It sets aria-current="page" and draws the row filled, bold and marked.
iconReactNode--Drawn before the label, and all that is drawn while the rail is collapsed. Decorative: the label names the row.

Keyboard and accessibility

The keyboard of Sidebar
KeysWhat it does
TabMoves to the toggle, then to each link.
EnterOn the toggle, expands or collapses the rail; on a link, follows it.
SpaceOn the toggle, expands or collapses the rail.
  • The toggle is a real button with aria-expanded, named by what a press will do: "Collapse the sidebar" or "Expand the sidebar".
  • current on one SidebarLink sets aria-current="page" and draws the row filled, bold and with a bar at its start.
  • Collapsed, the title, the label of each group and the label of each link are hidden on screen, and a screen reader still reads them.

Right to left

Under dir="rtl" the rail's line is on its left and the toggle on its right, with its icon mirrored. TopClassProvider with locale="ar" names both in Arabic.

Server and client

Renders on the server; the interactive part is a client component that ships only when it is used.

Files: index.tsx, client.tsx.

On the chip: split - size not measured yet.