A panel that slides in from one edge of the window over a scrim, with a header, a scrolling body and a footer of actions.
Options
| Choice | Values | Default | What it does |
|---|---|---|---|
positionsmbk-drawer--position-<value> | rightlefttopbottom | right | Which edge the panel comes from. |
inlinesmbk-drawer--inline | On or off | Off | Shown in place with no scrim, for previews and documentation. |
Examples
Usage
Header with title and description, a body, and a footer of actions. Shown in place here.
Positions
Four edges; the corners facing the page are rounded.
Without close
The close button can be hidden when an action closes instead.
Guidelines
Purpose
A panel that slides in from an edge of the window and sits over the page, for a task that needs more room than a modal but should not leave the page: filters, a settings form, the site navigation on a narrow screen. A modal interrupts in the middle; a drawer keeps the page visible beside it.
When to use
- Filters and settings that apply to the page behind.
- Detail of one row in a table, without leaving the table.
- The navigation on narrow screens.
When not to use
- A short confirmation. Use a modal.
- Content the reader needs alongside the page for a long time. Put it in the page.
Usage rules
Do
- Put the primary action last in the footer and give it the brand tone.
- Keep the title to a few words; it is the accessible name.
Don't
- Open a drawer on page load.
- Put a form's only submit button outside the drawer.
Put the primary action last and give it the brand tone, so it sits where the eye ends.
Leading with the brand button, and giving both the brand tone, muddles which one applies the change.
Anatomy
- The block, a native dialog element fixed to one edge, with a scrim behind it.
- The header part, holding the heading part (the title part with an optional description part straight under it), the close part centred on the title's first line, and a divider.
- The body part, which scrolls when the content is taller than the window.
- The footer part, optional: a divider and the actions part, buttons aligned to the end.
Behavior
Opens modally: the page behind is dimmed and cannot be used. Escape, the close button and a click on the scrim all ask to close: a drawer that holds its own open state closes itself, and one the page holds waits for the page to close it. A click anywhere on the panel, its edge included, never asks, nor does a press that starts on the panel and ends on the scrim, as when selecting words. With a popover, a dropdown or a list open inside the drawer, Escape and a click on the scrim close that first, and the drawer stays. The four positions change only where the panel sits, which corners are rounded and which edge it slides from. It slides in from its edge by its own size over a scrim that fades in, and slides back out a little faster as it closes; under reduced motion it appears in place and fades in and out instead of sliding, over the same scrim. Inline drawers do not move.
The inline flag renders the panel in place with no scrim, so it can be seen in the Showroom and in documentation.
The React wrapper opens the dialog modally for you, as other layers open: given open, the page holds the open state; without it, the drawer holds its own, closed unless given defaultOpen. Asking to close calls onOpenChange(false). Do not also set the dialog's open attribute.
Accessibility
What it does for you
- It is a native dialog opened modally: focus moves to the panel itself as it opens, so a screen reader names the drawer by its title, is held inside, and returns to the opener on close. Tab from the panel reaches the close button first.
- The panel is a container, not a control, so it draws no focus ring; the first control is not focused on opening, because a phone's browser may draw its ring after a tap and it would look pressed.
- It is labeled by its title and described by its description. The title is a second-level heading unless
headingLevelgives another. - The close button is icon-only and named "Close".
What you must do
- Give
headingLevelfor a drawer opened from deeper in the page's outline. - If the close button is hidden, make an action in the footer close the drawer.
- Give every word it says by itself, such as the close button's name, in the page's language with
closeLabel; it keeps its English unless the page gives its own. - Keep the title to a few words; it is the accessible name.
How to test
- Open it with the keyboard: focus moves into the drawer, the page behind cannot be reached, and the first Tab lands on the close button.
- Press Escape: the drawer asks to close, and once closed focus returns to the opener.
- With a screen reader, the drawer is read as a dialog by its title, followed by its description.
- With text at 200% and the page 320 pixels wide, the panel and its footer actions stay whole.
- In a forced-color mode, the panel's edge and the close button are still visible.
Keyboard
| Key | What it does |
|---|---|
| TaborShift+Tab | Moves focus through the controls inside the drawer. Focus is held inside while it is open. |
| Escape | Asks the page to close the drawer. |
| EnterorSpace | On the close button or a footer action, presses it. |
Source
The files the drawer is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
Stylesheet
/* smbk-drawer — Drawer. Figma "Drawer" 4212:23947 (Position: Left, Right, Top, Bottom) with "DrawerBody" 4237:27911.
* A panel that slides in from one edge over a scrim, built on the native
* dialog element. The width is the drawer width token, carried from Figma's frame.
* The inline flag shows the panel in place, for previews and documentation.
* The panel slides in from its edge over a fading scrim (motion rules at the end). */
.smbk-drawer {
box-sizing: border-box;
position: fixed;
inset: 0 0 0 auto;
z-index: var(--smbk-layer-modal);
display: none;
flex-direction: column;
width: var(--smbk-size-drawer-width);
max-width: 100%;
height: 100%;
max-height: none;
margin: 0;
padding: 0;
border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-rim);
border-radius: var(--smbk-radius-container-md) 0 0 var(--smbk-radius-container-md);
background: var(--smbk-color-surface-raised);
color: var(--smbk-color-text-default);
box-shadow: var(--smbk-elevation-overlay);
overflow: hidden;
}
.smbk-drawer[open] {
display: flex;
}
/* The panel takes focus as it opens, as a container, not a control: no ring of its own. */
.smbk-drawer:focus {
outline: none;
}
.smbk-drawer__header {
position: relative;
display: flex;
flex-direction: column;
gap: var(--smbk-space-stack-lg);
padding: var(--smbk-space-inset-2xl) var(--smbk-space-inset-2xl) 0;
}
/* The title sits straight on its description: the line heights give the air. */
.smbk-drawer__heading {
display: flex;
flex-direction: column;
gap: var(--smbk-space-text-label);
padding-right: var(--smbk-size-control-md);
}
.smbk-drawer__title {
margin: 0;
font-family: var(--smbk-typography-heading-2xs-bold-font-family);
font-size: var(--smbk-typography-heading-2xs-bold-font-size);
font-weight: var(--smbk-typography-heading-2xs-bold-font-weight);
line-height: var(--smbk-typography-heading-2xs-bold-line-height);
letter-spacing: var(--smbk-typography-heading-2xs-bold-letter-spacing);
}
.smbk-drawer__description {
margin: 0;
color: var(--smbk-color-text-subtlest);
font-family: var(--smbk-typography-body-sm-font-family);
font-size: var(--smbk-typography-body-sm-font-size);
font-weight: var(--smbk-typography-body-sm-font-weight);
line-height: var(--smbk-typography-body-sm-line-height);
letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
}
/* The close button is centred on the title's first line, its end edge the large inset from the panel's
end edge (the panel's hairline edge counted in), as Figma places it. */
.smbk-drawer__close {
position: absolute;
top: calc(var(--smbk-space-inset-2xl) + (var(--smbk-typography-heading-2xs-bold-font-size) * var(--smbk-typography-heading-2xs-bold-line-height) - var(--smbk-size-control-md)) / 2);
right: calc(var(--smbk-space-inset-lg) - var(--smbk-border-width-hairline));
}
.smbk-drawer__body {
flex: 1 1 auto;
min-height: 0;
padding: var(--smbk-space-inset-2xl);
overflow: auto;
/* The scrollbar every scrolling part of the system draws: thin, in the subtle border color, on a clear track. */
scrollbar-width: thin;
scrollbar-color: var(--smbk-color-border-subtle) transparent;
color: var(--smbk-color-text-subtle);
font-family: var(--smbk-typography-body-sm-font-family);
font-size: var(--smbk-typography-body-sm-font-size);
font-weight: var(--smbk-typography-body-sm-font-weight);
line-height: var(--smbk-typography-body-sm-line-height);
letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
}
/* The first thing in it, after the scrollbar the script adds (scrollbar.ts). */
.smbk-drawer__body > :first-child,
.smbk-drawer__body > .smbk-scrollbar:first-child + * {
margin-top: 0;
}
.smbk-drawer__body > :last-child {
margin-bottom: 0;
}
.smbk-drawer__footer {
display: flex;
flex-direction: column;
gap: var(--smbk-space-stack-lg);
padding: 0 var(--smbk-space-inset-2xl) var(--smbk-space-inset-2xl);
}
.smbk-drawer__actions {
display: flex;
justify-content: flex-end;
gap: var(--smbk-space-inline-lg);
}
/* ---- positions: the corners facing the page are rounded ---- */
.smbk-drawer--position-left {
inset: 0 auto 0 0;
border-radius: 0 var(--smbk-radius-container-md) var(--smbk-radius-container-md) 0;
}
.smbk-drawer--position-top,
.smbk-drawer--position-bottom {
width: 100%;
max-width: none;
height: auto;
max-height: 100%;
}
.smbk-drawer--position-top {
inset: 0 0 auto 0;
border-radius: 0 0 var(--smbk-radius-container-md) var(--smbk-radius-container-md);
}
.smbk-drawer--position-bottom {
inset: auto 0 0 0;
border-radius: var(--smbk-radius-container-md) var(--smbk-radius-container-md) 0 0;
}
/* ---- inline: shown in place, no scrim ---- */
.smbk-drawer--inline {
position: relative;
inset: auto;
z-index: auto;
height: auto;
}
.smbk-drawer--inline.smbk-drawer--position-top,
.smbk-drawer--inline.smbk-drawer--position-bottom {
width: var(--smbk-size-drawer-width);
}
/* ---- motion: the panel slides in from its own edge by its own size with the enter purpose,
over a scrim that fades in, and slides back out with the exit purpose. Under reduced motion the panel appears in
place and fades in and out with the same purposes, over the scrim that fades as before. The transitions, the scrim
and the page held still behind the drawer are the dialogs' own, shared with the modal and the command palette:
dialog/dialog.css; the slide from each edge is the drawer's. Inline drawers do not move. ---- */
/* Closed, and where it starts from as it opens: just past its own edge. */
.smbk-drawer:not(.smbk-drawer--inline):not([open]) {
translate: 100% 0;
}
.smbk-drawer--position-left:not(.smbk-drawer--inline):not([open]) {
translate: -100% 0;
}
.smbk-drawer--position-top:not(.smbk-drawer--inline):not([open]) {
translate: 0 -100%;
}
.smbk-drawer--position-bottom:not(.smbk-drawer--inline):not([open]) {
translate: 0 100%;
}
@starting-style {
.smbk-drawer[open]:not(.smbk-drawer--inline) {
translate: 100% 0;
}
.smbk-drawer--position-left[open]:not(.smbk-drawer--inline) {
translate: -100% 0;
}
.smbk-drawer--position-top[open]:not(.smbk-drawer--inline) {
translate: 0 -100%;
}
.smbk-drawer--position-bottom[open]:not(.smbk-drawer--inline) {
translate: 0 100%;
}
}
@media (prefers-reduced-motion: reduce) {
/* As heavy as each position's closed rule and later, so it wins over all four. */
.smbk-drawer:not(.smbk-drawer--inline):not([open]) {
opacity: 0;
translate: none;
}
@starting-style {
.smbk-drawer[open]:not(.smbk-drawer--inline) {
opacity: 0;
translate: none;
}
}
}Tokens it reads
| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| color.overlay.scrim--smbk-color-overlay-scrim | light: #00000080dark: #00000099changes with theme | Color | |
| motion.exit.duration--smbk-motion-exit-duration | 200ms | Motion | |
| motion.exit.easing--smbk-motion-exit-easing | cubic-bezier(0.7, 0, 0.84, 0) | Motion | |
| motion.enter.duration--smbk-motion-enter-duration | 320ms | Motion | |
| motion.enter.easing--smbk-motion-enter-easing | cubic-bezier(0.16, 1, 0.3, 1) | Motion | |
| layer.modal--smbk-layer-modal | 1300 | Stacking order | |
| size.drawer.width--smbk-size-drawer-width | 384px | Size | |
| border-width.hairline--smbk-border-width-hairline | 1px | Border width | |
| color.border.rim--smbk-color-border-rim | light: #ffffffe6dark: #ffffff0dchanges with theme | Color | |
| radius.container.md--smbk-radius-container-md | 12px | Radius | |
| color.surface.raised--smbk-color-surface-raised | light: #f9fafbdark: #232527changes with theme | Color | |
| color.text.default--smbk-color-text-default | light: #232527dark: #e7ebf0changes with theme | Color | |
| elevation.overlay--smbk-elevation-overlay | light: 0px 12px 16px 2px #373d431a, 0px 2px 8px 0px #373d4326dark: 0px 12px 16px 2px #00000026, 0px 2px 8px 0px #00000033 | Elevation | |
| space.stack.lg--smbk-space-stack-lg | comfortable: 16pxcompact: 12pxchanges with density | Spacing | |
| space.inset.2xl--smbk-space-inset-2xl | comfortable: 24pxcompact: 16pxchanges with density | Spacing | |
| space.text.label--smbk-space-text-label | 0px | Spacing | |
| size.control.md--smbk-size-control-md | comfortable: 40pxcompact: 32pxchanges with density | Size | |
| typography.heading-2xs-bold--smbk-typography-heading-2xs-bold-font-family--smbk-typography-heading-2xs-bold-font-size--smbk-typography-heading-2xs-bold-font-weight--smbk-typography-heading-2xs-bold-line-height--smbk-typography-heading-2xs-bold-letter-spacing--smbk-typography-heading-2xs-bold-text-transform | fontFamily: Funnel Display, Funnel Display Fallback, system-ui, sans-seriffontSize: min: 18px · max: 24pxfontWeight: 700lineHeight: 1.2letterSpacing: 0px | The quick brown fox | Typography |
| color.text.subtlest--smbk-color-text-subtlest | light: #606a75dark: #7c8692changes with theme | Color | |
| typography.body-sm--smbk-typography-body-sm-font-family--smbk-typography-body-sm-font-size--smbk-typography-body-sm-font-weight--smbk-typography-body-sm-line-height--smbk-typography-body-sm-letter-spacing--smbk-typography-body-sm-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 14pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| space.inset.lg--smbk-space-inset-lg | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| color.border.subtle--smbk-color-border-subtle | light: #c4cdd8dark: #2d3237changes with theme | Color | |
| color.text.subtle--smbk-color-text-subtle | light: #4e555edark: #9ba5b1changes with theme | Color | |
| space.inline.lg--smbk-space-inline-lg | comfortable: 12pxcompact: 8pxchanges with density | Spacing |
Manifest
{
"name": "drawer",
"block": "smbk-drawer",
"summary": "A panel that slides in from one edge of the window over a scrim, with a header, a scrolling body and a footer of actions.",
"adapt": "css",
"axes": {
"position": {
"values": [
"right",
"left",
"top",
"bottom"
],
"default": "right",
"description": "Which edge the panel comes from."
}
},
"flags": {
"inline": {
"description": "Shown in place with no scrim, for previews and documentation."
}
},
"parts": [
"header",
"heading",
"title",
"description",
"close",
"body",
"footer",
"actions"
],
"figmaNodeId": "4212:23947"
}