Skip to content

PlDrawer

A panel attached to one edge of the window. Two things in one component, because they are the same panel: the drawer you open, and the drawer that is simply part of the page.

React
tsx
import { PlButton, PlDrawer, PlDrawerClose } from 'plass-ui';

<PlDrawer side="right" trigger={<PlButton>Filters</PlButton>} title="Filters">
  Everything you can narrow by.
</PlDrawer>;
dart
import 'package:plass_ui/plass_ui.dart';

PlDrawer(
  side: PlassSide.right,
  open: filtering,
  onOpenChanged: (bool next) => setState(() => filtering = next),
  title: const Text('Filters'),
  child: const FilterForm(),
);

Props

PropTypeDefaultDescription
sideshared'top' | 'right' | 'bottom' | 'left''left'Which edge the panel is attached to. Physical rather than logical
mode'overlay' | 'inline''overlay'overlay is the drawer you open — a scrim, a focus trap, Escape. inline is a panel in the layout
openbooleanThe drawer is shown. Use with onOpenChange for a controlled drawer
defaultOpenbooleanWhether it starts open, for an uncontrolled one. false in overlay, true in inline
onOpenChange(open: boolean) => voidCalled when the open state changes
triggerReactElementThe element that opens the drawer. overlay only
titleReactNodeThe heading, and what names the drawer
descriptionReactNodeA line under the title, and the drawer's accessible description
actionsReactNodeThe bottom row, held against the foot of the panel. Laid out end-aligned
dividersbooleanfalseScores the panel between its sections with a hairline instead of separating them with space
showClosebooleanShows the × in the corner. On in overlay, off in inline
closeLabelstring'Close'Accessible name of the × button
extentnumber | stringHow far the panel reaches in from its edge: a width for left/right, a height for top/bottom
roundedbooleantrueRounds the two corners that face the page. The ones against the window edge are always square
modalboolean | 'trap-focus'trueWhether the page behind is taken away. 'trap-focus' keeps it usable while still holding focus inside
dismissiblebooleantrueWhether Escape or a press on the scrim closes it. overlay only
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'The panel's width, radius and padding
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. It reaches the focus rings inside and nothing else
densityshared'default' | 'compact''default'How tightly the sections pack
classNames{ backdrop?: string }Classes on the parts a className does not reach. backdrop is the scrim drawn behind the surface
PropTypeDefaultDescription
open * boolThe drawer is shown. Use with onOpenChange for a controlled drawer
onOpenChangedValueChanged<bool>?Called with what the open state should become. The × and a press outside report rather than act
childWidget?The body — the only part that scrolls
sidesharedPlassSidePlassSide.leftWhich edge the panel is attached to. Physical rather than logical
modePlDrawerModePlDrawerMode.overlayoverlay is the drawer you open — a scrim, a focus trap, Escape. inline is a panel in the layout
titleWidget?The heading, and what names the drawer
descriptionWidget?A line under the title, and the drawer's accessible description
actionsList<Widget>?The bottom row, held against the foot of the panel. Laid out end-aligned
dividersboolfalseScores the panel between its sections with a hairline instead of separating them with space
showClosebool?Shows the × in the corner. On in overlay, off in inline
closeLabelString'Close'Accessible name of the × button
extentdouble?How far the panel reaches in from its edge, in logical pixels: a width for left/right, a height for top/bottom
roundedbooltrueRounds the two corners that face the page. The ones against the window edge are always square
modalbooltrueWhether the screen behind is taken away for the pointer as well as the keyboard
dismissiblebooltrueWhether Escape or a press on the scrim closes it. overlay only
sizesharedPlassSizePlassSize.mdThe panel's width, radius and padding
colorsharedPlassColorPlassColor.primarySemantic colour role. It reaches the focus rings inside and nothing else
densitysharedPlassDensityPlassDensity.standardHow tightly the sections pack

Every other <div> attribute passes through to the panel, and so does a className. The scrim an overlay drawer draws behind it is a second element in the same portal. classNames.backdrop is the way to reach it, and an inline drawer has none for it to land on.

What the shared axes mean across the library is in prop conventions.

Two modes, one panel

mode is what separates them, and it is a separate axis from variant, which already means the weight of a surface across the whole library and would be a second spelling of nothing.

  • overlay. It is opened, it floats over the page on a scrim, it holds the focus, and it is dismissed. The navigation drawer behind a hamburger, the filter panel beside a table.
  • inline. It is part of the layout and the page is laid out around it. No scrim, no focus trap, nothing to dismiss. The sidebar that is simply there.

Everything else about them is identical, which is exactly why they are not two components a caller has to switch between when a sidebar becomes a hamburger at a breakpoint.

defaultOpen follows: false in overlay, true in inline, because a fixed sidebar that had to be opened before it appeared would not be a fixed sidebar.

React

No variant and no elevation

The three materials answer "how much does this surface assert itself against the page", and a panel that has taken an edge of the window has answered it. An overlay drawer floats and carries a shadow at the top of the ladder; an inline one is part of the layout and carries none. Neither is a decision worth offering.

Examples

side

Physical rather than logical, the way PlassSide is everywhere: a drawer along the top of the window is along the top in every writing direction.

The panel is square against the window and cut on the free side, the corners that face the page take the house fillet, the two against the edge do not, because a corner cut off something with no visible end is a corner cut off nothing. The hairline follows the same rule and is drawn on the free edge only.

A left or right panel takes the width its size implies; a top or bottom one is as tall as what is in it, up to 85% of the window. A bottom sheet holding three rows should be three rows tall. extent overrides either.

React

Nothing slides

The panel fades, and only fades. A drawer that slid in would be dragging its own text across the screen for the length of the transition, and a panel is nothing but text and controls, so this is the case the no-transform rule was written for rather than the exception to it.

What says the panel came from an edge is that it is attached to one.

dividers

Scores the panel between the header, the body and the actions with a hairline instead of separating them with space. Worth turning on the moment the body scrolls: the lines are what say the header stayed put.

The body is the only part that scrolls either way.

Accessibility

  • An overlay drawer holds the focus while it is up, puts it back where it came from on the way out, and takes the screen behind it away.
  • title names it and description describes it. Both wired to the panel rather than sitting near it, and the title is announced as a heading.
  • An inline drawer is not a dialog and claims none of that. It is a panel in the layout, and its heading is an ordinary one.
  • dismissible={false} refuses both Escape and a press on the scrim. Give a drawer that refuses them actions that answer it, because there will be no other way out.
  • Base UI owns the focus trap, the scroll lock, the aria-labelledby / aria-describedby wiring and the inert page behind. modal="trap-focus" keeps the page scrollable and clickable while still holding focus inside.
  • PlDrawerClose exists so an uncontrolled drawer's Cancel button has something to call. render makes it a real Plass button: <PlDrawerClose render={<PlButton variant="ghost">Cancel</PlButton>} />.
  • The lift, the scrim, the focus scope, Escape and focus going back where it came from are PlassPortal's. The same layer a PlModal and a PlOverlay are built on, so a drawer opened over an overlay shows no seam.

Differences from the React build

ReactFlutterWhy
open / defaultOpen / onOpenChangeopen / onOpenChangedFlutter's own controls are controlled, and so is every stateful widget in this package.
triggerThere is nothing to wire a trigger up to here: the app opens the drawer by setting open, and the button that does it is the app's own.
PlDrawerCloseIt exists over there so an uncontrolled drawer's Cancel button has something to call. Every drawer here is controlled, so the button already has onOpenChanged.
extent: number | stringextent: doublePixels stay pixels. There is no CSS length to accept.
modal: boolean | 'trap-focus'modal: boolThe two values that differ are "the pointer is held out" and "it is not". Flutter has no scroll lock to be the third thing.
className, styleThere is no class list and no style attribute to pass through.

Released under the MIT License