# Drawer

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.

Block: `smbk-drawer`. Status: experimental. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `position` | `right`, `left`, `top`, `bottom` | `right` | Which edge the panel comes from. |
| `inline` | on / off | off | Shown in place with no scrim, for previews and documentation. |

Parts: `smbk-drawer__header`, `smbk-drawer__heading`, `smbk-drawer__title`, `smbk-drawer__description`, `smbk-drawer__close`, `smbk-drawer__body`, `smbk-drawer__footer`, `smbk-drawer__actions`

## Markup

```html
<dialog
  open=""
  class="smbk-drawer smbk-drawer--position-right smbk-drawer--inline"
  aria-labelledby="drawer-1-title"
  aria-describedby="drawer-1-description"
>
  <div class="smbk-drawer__header">
    <div class="smbk-drawer__heading">
      <h2 id="drawer-1-title" class="smbk-drawer__title">Filters</h2>
      <p id="drawer-1-description" class="smbk-drawer__description">Narrow the list of projects.</p>
    </div>
    <button
      type="button"
      class="smbk-btn smbk-btn--tone-ghost smbk-btn--size-md smbk-btn--icon-only smbk-drawer__close"
      aria-label="Close"
    >
      <i class="ph ph-x smbk-icon smbk-icon--size-md smbk-btn__icon" aria-hidden="true"></i>
    </button>
    <hr class="smbk-divider smbk-divider--orientation-horizontal"/>
  </div>
  <div
    class="smbk-drawer__body"
    data-scrollbar=""
  >Choose a status, an owner and a date range. The list of projects updates as soon as you apply the filters, and the filters stay in place until you reset them.</div>
  <div class="smbk-drawer__footer">
    <hr class="smbk-divider smbk-divider--orientation-horizontal"/>
    <div class="smbk-drawer__actions">
      <button type="button" class="smbk-btn smbk-btn--tone-outline smbk-btn--size-md">
        <span class="smbk-btn__label">Reset</span>
      </button>
      <button type="button" class="smbk-btn smbk-btn--tone-brand smbk-btn--size-md">
        <span class="smbk-btn__label">Apply filters</span>
      </button>
    </div>
  </div>
</dialog>
```

## React

```tsx
import { Drawer } from './design-system/components/drawer/drawer.js';
import { Button } from './design-system/components/button/button.js';

<Drawer position="right" inline open title="Filters" description="Narrow the list of projects." actions={<Button tone="outline">Reset</Button>
<Button tone="brand">Apply filters</Button>}>Choose a status, an owner and a date range. The list of projects updates as soon as you apply the filters, and the filters stay in place until you reset them.</Drawer>
```

## Stylesheet

```css
/* 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

```json
{
  "color": {
    "overlay": {
      "scrim": {
        "$type": "color",
        "$value": "{{color.alpha.black.50}}",
        "$description": "Dimmed backdrop behind dialogs and drawers."
      }
    },
    "border": {
      "rim": {
        "$type": "color",
        "$value": "{{color.alpha.white.90}}",
        "$description": "The light rim that makes a surface look lit: fields, chosen pills, cards."
      },
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.300}}",
        "$description": "Dividers and quiet edges."
      }
    },
    "surface": {
      "raised": {
        "$type": "color",
        "$value": "{{color.grey.50}}",
        "$description": "One floor above default: popovers, raised panels."
      }
    },
    "text": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "subtlest": {
        "$type": "color",
        "$value": "{{color.grey.600}}",
        "$description": "Quietest text: hints, captions, placeholders."
      },
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Secondary text: descriptions, metadata."
      }
    }
  },
  "motion": {
    "exit": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.200}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.expo-in}}"
      }
    },
    "enter": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.320}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.expo-out}}"
      }
    }
  },
  "layer": {
    "modal": {
      "$type": "number",
      "$value": 1300
    }
  },
  "size": {
    "drawer": {
      "width": {
        "$type": "dimension",
        "$value": "384px",
        "$description": "Width of a drawer from the left or right edge. A fixed frame width in Figma with no variable behind it; it should become one."
      }
    },
    "control": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.40}}",
        "$description": "Height of a medium control, where controls insert by default: 40 comfortable, 32 compact. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "radius": {
    "container": {
      "md": {
        "$type": "dimension",
        "$value": "{{radius.12}}",
        "$description": "Corners of cards, panels and dialogs, medium."
      }
    }
  },
  "elevation": {
    "overlay": {
      "$type": "shadow",
      "$value": {
        "light": "0px 12px 16px 2px #373d431a, 0px 2px 8px 0px #373d4326",
        "dark": "0px 12px 16px 2px #00000026, 0px 2px 8px 0px #00000033"
      }
    }
  },
  "space": {
    "stack": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.16}}",
        "$description": "Gap between things stacked one above another, large."
      }
    },
    "inset": {
      "2xl": {
        "$type": "dimension",
        "$value": "{{space.24}}",
        "$description": "Padding inside a component, 2× large."
      },
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Padding inside a component, large."
      }
    },
    "text": {
      "label": {
        "$type": "dimension",
        "$value": "{{space.0}}",
        "$description": "Between a label or title and the line of supporting text right under it, inside a component. None: untrimmed line height already spaces the two lines."
      }
    },
    "inline": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Gap between things side by side, large."
      }
    }
  },
  "typography": {
    "heading-2xs-bold": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Funnel Display, Funnel Display Fallback, system-ui, sans-serif",
        "fontSize": {
          "min": "18px",
          "max": "24px"
        },
        "fontWeight": 700,
        "lineHeight": 1.2,
        "letterSpacing": "0px"
      },
      "$description": "Heading 2XS in bold: drawer and modal titles."
    },
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  }
}
```

## 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.

## 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.

## 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](modal.md).
- Content the reader needs alongside the page for a long time. Put it in the page.

## 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 `headingLevel` gives another.
- The close button is icon-only and named "Close".

### What you must do

- Give `headingLevel` for 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 |
|---|---|
| Tab or Shift + 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. |
| Enter or Space | On the close button or a footer action, presses it. |

## 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.
