Components

Drawer

Headless modal side-sheet primitives with drawer-specific parts and placement metadata.

When to Use

Use Drawer when a modal task or navigation panel should enter from an edge of the screen. Use Dialog when the panel belongs in the center or has no edge meaning. Use an ordinary inline panel when the page should remain fully usable while it is open. Drawer provides behavior and placement metadata, but your application owns its visual position and motion.

Features

  • Supports controlled and uncontrolled open state with close reasons.
  • Traps and restores focus, locks scrolling, and contains descendant portals.
  • Supports independent Escape, backdrop, and Overlay dismissal controls.
  • Exposes consumer-provided placement through [data-placement].
  • Supports keep-mounted content for consumer-owned transitions.
  • Provides generated title and description relationships.
  • Remains headless: placement does not apply layout or animation.

Import

import { Drawer } from "@flowstack-ui/atom";

Anatomy

<Drawer.Root>
  <Drawer.Trigger />
  <Drawer.Portal>
    <Drawer.Overlay />
    <Drawer.Content>
      <Drawer.Title />
      <Drawer.Description />
      <Drawer.Close />
    </Drawer.Content>
  </Drawer.Portal>
</Drawer.Root>

API Reference

Root

Owns drawer state, dismissal settings, generated IDs, and focus restoration. It renders no wrapper.

PropTypeDefault
openboolean-
defaultOpenbooleanfalse
onOpenChange(open: boolean, reason?: ModalCloseReason) => void-
closeOnEscapebooleantrue
closeOnBackdropClickbooleantrue
disabledbooleanfalse
keepMountedbooleanfalse

Trigger

Opens the drawer. It renders a native button by default and supplies button semantics when composed onto a custom element.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
role"button" for custom elements
aria-haspopup"dialog"
aria-expandedCurrent open state
aria-controlsContent ID while open
aria-disabled"true" when Root is disabled
Data attributeValues
[data-slot]"drawer-trigger"
[data-state]"open" | "closed"
[data-disabled]Present when disabled

Portal

Moves Overlay and Content to document.body by default without creating a wrapper.

PropTypeDefault
containerElement | DocumentFragment | nulldocument.body
disabledbooleanfalse

Overlay

Renders the accessibility-hidden backdrop. Clicking it closes with reason "backdropClick" unless Root or this Overlay disables backdrop dismissal.

PropTypeDefault
disabledbooleanfalse
ARIA attributeValues
aria-hidden"true"
Data attributeValues
[data-slot]"drawer-overlay"
[data-state]"open" | "closed"
[data-positioned]Present after the opening frames

Content

Renders the focus-trapped panel as a div. placement is copied to a data attribute only; it does not apply positioning.

PropTypeDefault
placementstring-
ariaLabelstring-
ARIA attributeValues
role"dialog"
aria-modal"true" while visible
aria-labelValue from ariaLabel
aria-labelledbyGenerated Title ID when ariaLabel is absent
aria-describedbyGenerated Description ID
Data attributeValues
[data-slot]"drawer-content"
[data-state]"open" | "closed"
[data-placement]Consumer-provided placement string
[data-positioned]Present after the opening frames

With keepMounted, closed Content remains inside a hidden, aria-hidden wrapper and preserves its class name and placement metadata.

Title

Provides the heading referenced by Content. It renders an h2 by default.

PropTypeDefault
as"h1" | "h2" | "h3" | "h4" | "h5" | "h6""h2"
Data attributeValues
[data-slot]"drawer-title"

Description

Provides supporting text referenced by Content. It renders a native p.

Data attributeValues
[data-slot]"drawer-description"

Close

Closes Root with reason "closeClick". It renders a native button by default and supports custom composition.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"drawer-close"

The Drawer entry point also exports useModalContext and useModalContent for advanced custom modal parts. The namespaced parts are preferred for ordinary drawers because they provide the complete contract above.

Examples

import { Drawer } from "@flowstack-ui/atom";

export function NavigationDrawer() {
  return (
    <Drawer.Root>
      <Drawer.Trigger>Open navigation</Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Overlay />
        <Drawer.Content placement="left">
          <Drawer.Title>Navigation</Drawer.Title>
          <Drawer.Description>Choose an area of the application.</Drawer.Description>
          <nav aria-label="Primary">
            <a href="/projects">Projects</a>
            <a href="/settings">Settings</a>
          </nav>
          <Drawer.Close>Close navigation</Drawer.Close>
        </Drawer.Content>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

Controlled Drawer

import { useState } from "react";
import { Drawer } from "@flowstack-ui/atom";

export function ControlledDrawer() {
  const [open, setOpen] = useState(false);

  return (
    <Drawer.Root open={open} onOpenChange={setOpen}>
      <Drawer.Trigger>Show filters</Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Overlay />
        <Drawer.Content placement="right">
          <Drawer.Title>Filters</Drawer.Title>
          <Drawer.Description>Narrow the visible results.</Drawer.Description>
          <Drawer.Close>Apply filters</Drawer.Close>
        </Drawer.Content>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

Accessibility

Drawer uses the WAI-ARIA Modal Dialog pattern. Give every drawer a Title or ariaLabel, and add Description when the purpose needs more explanation. Focus moves inside on open, stays within the drawer and its registered descendant portals, and returns after close.

KeyDescription
EscapeCloses the topmost drawer when closeOnEscape is enabled.
TabMoves to the next focusable element in the drawer scope.
Shift+TabMoves to the previous focusable element in the drawer scope.
Enter / SpaceOpens from Trigger and activates native button controls.

Changelog

See CHANGELOG.md.