Skip to content

PlSidebar

A column beside the page's content, and a drawer once the window is too narrow to hold one. Two presentations of one panel, so a caller never swaps components at a breakpoint.

React
tsx
import { PlPageLayout, PlSidebar } from 'plass-ui';

<PlPageLayout sidebar={<PlSidebar label="Main navigation">{nav}</PlSidebar>}>{page}</PlPageLayout>;
dart
import 'package:plass_ui/plass_ui.dart';

PlPageLayout(
  sidebar: PlSidebar(semanticLabel: 'Main navigation', child: navigation),
  child: page,
);

Props

PropTypeDefaultDescription
variantshared'solid' | 'glass' | 'ghost''glass'What the panel is made of. Never dyed — what is on it arrives with colours of its own
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
side'start' | 'end''start'Which end of the band it takes. Logical rather than physical. Inside a PlPageLayout the slot it was handed to has already decided this
widthnumber | stringHow wide the column is — a number in pixels or any CSS length. Left out, it is the width size implies
minWidthnumber | string160How narrow it may be dragged
maxWidthnumber | string480And how wide
resizablebooleanfalseLets the reader drag the inner edge to change the column's width
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'The window width below which it becomes a drawer instead of a column. Defaults to the PlPageLayout's own, and to none outside one
openbooleanWhether the drawer is open. Only meaningful once collapsed; inside a PlPageLayout the layout owns it
defaultOpenbooleanfalseWhich state it starts in, for an uncontrolled standalone sidebar
onOpenChange(open: boolean) => voidCalled when the drawer opens or closes
stickybooleantrueWhether the column holds its place while the page scrolls past it. It costs nothing when it is not needed
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
dividerbooleantrueDraws a hairline down the inner edge — the one facing the content
paddedbooleantrueThe gutter and the air above and below the content
labelstring'Sidebar'The name the region is announced by. A page with two sidebars must write one
closeLabelstring'Close sidebar'What the drawer's close button says, once the sidebar has collapsed
resizeLabelstring'Resize sidebar'What the drag handle is announced as
childrenReactNodeEverything in it: a nav, a filter panel, a table of contents
PropTypeDefaultDescription
variantsharedPlassVariantPlassVariant.glassWhat the panel is made of. Never dyed — what is on it arrives with colours of its own
sizesharedPlassSizePlassSize.mdThe panel's default width and the air around its content
colorsharedPlassColorPlassColor.primarySemantic colour role. Arbitrary colour values are not accepted
densitysharedPlassDensityPlassDensity.standardPadding only — never the height, never the type scale
elevationsharedint0Drop shadow depth. 0 means no shadow at all
sidePlassSidebarSide?PlassSidebarSide.startWhich end of the band it takes. Logical rather than physical. Inside a PlPageLayout the slot it was handed to has already decided this
widthdouble?How wide the column is — a number in pixels or any CSS length. Left out, it is the width size implies
minWidthdouble160How narrow it may be dragged
maxWidthdouble480And how wide
resizableboolfalseLets the reader drag the inner edge to change the column's width
onResizeValueChanged<double>?Fires with the width in pixels while the edge is being dragged
onResizeEndValueChanged<double>?Fires once, with the same number, when it is let go
collapseBelowPlassBreakpoint?The width below which the column becomes a drawer, measured against the window. Left out, the PlPageLayout above decides — and outside a layout it never collapses
openbool?Whether the drawer is open. Only meaningful once collapsed; inside a PlPageLayout the layout owns it
onOpenChangedValueChanged<bool>?Called when the drawer opens or closes
titleWidget?The heading, drawn only while the sidebar is a drawer. Left out, semanticLabel is drawn as the heading instead: a panel that has covered the screen is named by what it draws
dividerbooltrueDraws a hairline down the inner edge — the one facing the content
paddedbooltrueThe gutter and the air above and below the content
semanticLabelString'Sidebar'The name the region is announced by. A page with two sidebars must write one
closeLabelString'Close sidebar'What the drawer's close button says, once the sidebar has collapsed
resizeLabelString'Resize sidebar'What the drag handle is announced as
childWidget?Everything in it: a nav, a filter panel, a table of contents

Every native <aside> attribute passes straight through. color and title are excluded because both are Plass props here.

PlSidebarTrigger

PropTypeDefaultDescription
side'start' | 'end''start'Which of the layout's two sidebars it opens
collapseBelow'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'none'The width below which the button appears — the same one the sidebar collapses at. Inherited from the PlPageLayout
iconReactNodeThe glyph. A hamburger, drawn here, unless something else is given
labelstring'Open sidebar' / 'Close sidebar'What it does, in words
PropTypeDefaultDescription
sidePlassSidebarSidePlassSidebarSide.startWhich of the layout's two sidebars it opens
iconWidget?The glyph. A hamburger, drawn here, unless something else is given
labelString?'Open sidebar' / 'Close sidebar'What it does, in words
variantsharedPlassVariantPlassVariant.ghostWhat the key is made of. ghost by default: it sits on a bar that is already a sheet
sizesharedPlassSizePlassSize.mdThe key's size
colorsharedPlassColorPlassColor.primarySemantic colour role

Everything else is PlIconButton's, unchanged.

What the shared axes (variant size color density elevation) mean across the library is in prop conventions.

Two shapes, one panel

Above collapseBelow the sidebar is an <aside> in the layout that the content is laid out around. Below it, the same children are a PlDrawer over a scrim, with a focus trap, an Esc and a way back to the trigger.

They are one component because they are one thing, and because the children then exist once either way, rather than being rendered twice into the document for a screen reader to read twice.

Which of the two is showing is a media query, answered in CSS for the first paint and in JavaScript from then on. The markup a server sends is the column, so a narrow screen would otherwise draw a full-width sidebar and throw it away a moment later. The class that hides it below the breakpoint is what stops that, and matchMedia decides, once there is a window to ask, that the drawer should exist at all.

Examples

side

Logical rather than physical: start is the left of an English page and the right of an Arabic one, because a navigation rail is beside the text it belongs to in every writing direction.

Inside a PlPageLayout it is already decided by which slot the sidebar was handed to, and setting it again is only a way of disagreeing with the layout.

React

collapseBelow

The window width below which the column becomes a drawer. It defaults to the layout's own collapseBelow, and to none outside a layout, a sidebar that collapsed with nothing on the page able to bring it back would be a sidebar the reader has lost.

PlSidebarTrigger is what brings it back. Put it in a PlHeader's brand slot, ahead of the logo, which is where thirty years of hamburgers have taught readers to look. It is hidden by the same media query rather than by a piece of state, so it is in the markup a server sends rather than popping into the header a moment after the page arrives.

title is drawn only while the sidebar is a drawer: a column has the page around it to say what it is, and a panel that has covered the page does not.

React

resizable

Off by default. A sidebar that can be resized is a sidebar whose width is the reader's to remember, so a caller who turns this on usually also stores what onResizeEnd reports.

The dragged width is written straight onto the element rather than into state: nothing in the tree depends on the number except one CSS declaration, and a setState per pointer move would re-render every row in the panel. The caller still hears every step through onResize.

The handle straddles the edge rather than sitting inside it, a hairline one pixel wide is a target one pixel wide, which is the same split between what is drawn and what can be grabbed that a scrollbar makes.

React

variant

The three materials, read the way a container reads them. The panel is never dyed: what is on a sidebar is somebody's navigation, and it arrives with colours of its own.

divider rules the inner edge, the one facing the content. The outer edge is against the window, where there is nothing on the other side to be separated from.

React

sticky

On by default, and it costs nothing when it is not needed. With the page scrolling, the column is sticky and as tall as what is left of the window under the header, which is what --p-layout-header and --p-layout-footer are measured for. With only the content scrolling, the column is already as tall as the layout and this changes nothing.

Differences from the React build

ReactFlutterWhy
collapseBelow against the window, defaulting to the layout'sthe same, but the layout's answer is against its own widthA LayoutBuilder sees the constraints the layout was handed; a media query only ever sees the window. Set it here and the window is what is measured, which is the override.
'none'nullDart's way of saying "no floor was named".
the trigger hidden by a media querythe trigger not built at allThe class exists on the web to keep the button in the markup a server sends. There is no first paint to hold together here.
stickyThe column is as tall as the band the layout gave it. There is no document scroll for it to hold its place against.
title falling back to an aria-labeltitle falling back to semanticLabel, drawnA PlDrawer is named by what it draws, so the region's name becomes its heading rather than an invisible label.
a role="separator" handle with aria-valuenowa Semantics(slider: true) handle with a value in logical pixelsFlutter's semantics tree has no separator role and no valuenow. A handle is what it actually is: a control with a value that can be turned up and down.
the width written onto the elementthe width in a ValueNotifierSame decision, different spelling: nothing but one box depends on the number, and rebuilding the panel on every pointer move would rebuild every row in it.
labelsemanticLabelFlutter's name.
className, style, native attributesThere is no class list and no style attribute to pass through.

Accessibility

  • The column is a real <aside>, which is the complementary landmark.
  • label is required in practice and defaults to Sidebar. A page with two sidebars must give each one a name, or a screen reader offers two regions called "complementary".
  • Collapsed, it is a dialog: focus is trapped, Esc closes it, the page behind it is inert, and focus returns to whatever opened it. All of that is PlDrawer's, which is Base UI's.
  • The trigger carries aria-expanded, so a screen reader is told whether the panel is open before it is pressed.
  • The resize handle is a role="separator" with aria-orientation="vertical", a tab stop while resizable, and moved by . A key press fires onResizeEnd as well as onResize, because it is a whole gesture on its own.
  • A drag takes the page's text selection away as -webkit-user-select, the only name WebKit implements, rather than calling preventDefault, which would stop the browser focusing the handle.
  • The column claims SemanticsRole.complementary, the same landmark the <aside> tag carries on the other side.
  • semanticLabel names it and defaults to Sidebar. A screen with two sidebars must name each one: Flutter refuses a duplicated landmark with no label outright.
  • Collapsed, it is a PlDrawer: focus is trapped, the barrier dismisses it, and focus returns to whatever opened it.
  • The resize handle is a Semantics(slider: true) with the width as its value and onIncrease / onDecrease wired to the same step the arrow keys use, so a screen reader can move the edge without a pointer.
  • The trigger is a real PlIconButton with a name that says what pressing it will do.

Released under the MIT License