Skip to main content

Drawer

Drawer is a slide-in side panel that wraps Spar's Dialog primitive in modal mode. It provides a compound component API for building navigation menus, detail views, filters, or contextual forms that overlay the current page.

Usage​

import { Drawer } from '@takeoff-ui/react-spar';
<Drawer>
<Drawer.Trigger />
<Drawer.Overlay />
<Drawer.Panel>
<Drawer.Header>
<Drawer.Title />
<Drawer.Close />
</Drawer.Header>
<Drawer.Body>
<Drawer.Description />
</Drawer.Body>
<Drawer.Footer />
</Drawer.Panel>
</Drawer>

Playground​


function PlaygroundDemo() {
  return (
    <Drawer placement="right">
      <Drawer.Trigger as={Button}>Open Drawer</Drawer.Trigger>
      <Drawer.Overlay />
      <Drawer.Panel>
        <Drawer.Header>
          <Drawer.Title>Flight Details</Drawer.Title>
          <Drawer.Close />
        </Drawer.Header>
        <Drawer.Body>
          <Drawer.Description>
            Review your selected flight information and passenger details before confirming the booking.
          </Drawer.Description>
        </Drawer.Body>
      </Drawer.Panel>
    </Drawer>
  );
}

render(<PlaygroundDemo />);

Placement​

The placement prop controls from which side the drawer slides in.


function PlacementDemo() {
  const [placement, setPlacement] = useState('right');

  return (
    <div className="flex flex-wrap justify-center gap-3">
      {['left', 'right', 'top', 'bottom'].map((p) => (
        <Drawer key={p} placement={p}>
          <Drawer.Trigger as={Button}>{p}</Drawer.Trigger>
          <Drawer.Overlay />
          <Drawer.Panel>
            <Drawer.Header>
              <Drawer.Title>Drawer from {p}</Drawer.Title>
              <Drawer.Close />
            </Drawer.Header>
            <Drawer.Body>
              This drawer slides in from the <strong>{p}</strong> side.
            </Drawer.Body>
          </Drawer.Panel>
        </Drawer>
      ))}
    </div>
  );
}

render(<PlacementDemo />);

Full Screen​

Stretch Drawer.Panel to the viewport with a style override to fill the whole screen. placement still decides which edge the panel slides in from.


function FullScreenDemo() {
  const fullScreen = {
    inset: 0,
    width: '100%',
    height: '100%',
    border: 'none',
    borderRadius: 0,
  };

  return (
    <div className="flex flex-wrap justify-center gap-3">
      {['right', 'bottom'].map((p) => (
        <Drawer key={p} placement={p}>
          <Drawer.Trigger as={Button}>Full screen from {p}</Drawer.Trigger>
          <Drawer.Overlay />
          <Drawer.Panel style={fullScreen}>
            <Drawer.Header headerType="divided">
              <Drawer.Title>Full-screen drawer</Drawer.Title>
              <Drawer.Close />
            </Drawer.Header>
            <Drawer.Body>
              The panel is stretched to the viewport by the style override and
              still slides in from the <strong>{p}</strong> edge.
            </Drawer.Body>
            <Drawer.Footer footerType="divided">
              <Drawer.Close as={Button} variant="secondary">Close</Drawer.Close>
            </Drawer.Footer>
          </Drawer.Panel>
        </Drawer>
      ))}
    </div>
  );
}

render(<FullScreenDemo />);

Drawer.Footer supports a few visual variants through the footerType prop. Use divided to add a separation line or light to soften the footer background.


function FooterTypeDrawerDemo() {
  const footerTypes = ['basic', 'divided', 'light'];

  return (
    <div className="flex flex-wrap justify-center gap-3">
      {footerTypes.map((type) => (
        <Drawer key={type} placement="right">
          <Drawer.Trigger as={Button}>{type}</Drawer.Trigger>
          <Drawer.Overlay />
          <Drawer.Panel>
            <Drawer.Header>
              <Drawer.Title>{type} footer</Drawer.Title>
              <Drawer.Close />
            </Drawer.Header>
            <Drawer.Body>
              This footer uses <code>{type}</code> styling.
            </Drawer.Body>
            <Drawer.Footer footerType={type}>
              <Button variant="secondary" appearance="outlined">Cancel</Button>
              <Button>Confirm</Button>
            </Drawer.Footer>
          </Drawer.Panel>
        </Drawer>
      ))}
    </div>
  );
}

render(<FooterTypeDrawerDemo />);

Header Types​

Drawer.Header supports multiple visual variants via the headerType prop. Use this option to add a divider, light or dark background, or a primary-style header when the drawer content requires extra emphasis.


function HeaderTypeDrawerDemo() {
  const headerTypes = ['basic', 'divided', 'light', 'dark', 'primary'];

  return (
    <div className="flex flex-wrap justify-center gap-3">
      {headerTypes.map((type) => (
        <Drawer key={type} placement="right">
          <Drawer.Trigger as={Button}>{type}</Drawer.Trigger>
          <Drawer.Overlay />
          <Drawer.Panel>
            <Drawer.Header headerType={type}>
              <Drawer.Title>{type} header</Drawer.Title>
              <Drawer.Close />
            </Drawer.Header>
            <Drawer.Body>
              Header type: {type}
            </Drawer.Body>
          </Drawer.Panel>
        </Drawer>
      ))}
    </div>
  );
}

render(<HeaderTypeDrawerDemo />);

Dismissible​

Set dismissible={false} to prevent the drawer from closing when the user clicks outside or presses Escape.


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

  return (
    <Drawer open={open} onOpenChange={setOpen} dismissible={false} placement="right">
      <Drawer.Trigger as={Button}>Open non-dismissible</Drawer.Trigger>
      <Drawer.Overlay />
      <Drawer.Panel>
        <Drawer.Header>
          <Drawer.Title>Sticky Drawer</Drawer.Title>
          <Drawer.Close />
        </Drawer.Header>
        <Drawer.Body>
          This drawer can only be closed using the close control.
        </Drawer.Body>
      </Drawer.Panel>
    </Drawer>
  );
}

render(<DismissibleDrawerDemo />);

Overlay Behavior​

Use blur for a softened backdrop and invisible when the overlay should stay interactive but visually disappear.


function OverlayBehaviorDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-3">
      <Drawer placement="right">
        <Drawer.Trigger as={Button}>Blur</Drawer.Trigger>
        <Drawer.Overlay blur />
        <Drawer.Panel>
          <Drawer.Header>
            <Drawer.Title>Blurred overlay</Drawer.Title>
            <Drawer.Close />
          </Drawer.Header>
          <Drawer.Body>
            The backdrop stays visible and adds a soft blur effect behind the panel.
          </Drawer.Body>
        </Drawer.Panel>
      </Drawer>

      <Drawer placement="right">
        <Drawer.Trigger as={Button}>Invisible</Drawer.Trigger>
        <Drawer.Overlay invisible />
        <Drawer.Panel>
          <Drawer.Header>
            <Drawer.Title>Invisible overlay</Drawer.Title>
            <Drawer.Close />
          </Drawer.Header>
          <Drawer.Body>
            The overlay remains mounted for interaction but is visually hidden.
          </Drawer.Body>
        </Drawer.Panel>
      </Drawer>
    </div>
  );
}

render(<OverlayBehaviorDemo />);

Intensity​

The intensity prop controls how dark the overlay appears.


function OverlayIntensityDemo() {
  const intensities = ['lightest', 'light', 'base', 'dark', 'darkest'];

  return (
    <div className="flex flex-wrap justify-center gap-3">
      {intensities.map((intensity) => (
        <Drawer key={intensity} placement="right">
          <Drawer.Trigger as={Button}>{intensity}</Drawer.Trigger>
          <Drawer.Overlay intensity={intensity} />
          <Drawer.Panel>
            <Drawer.Header>
              <Drawer.Title>{intensity}</Drawer.Title>
              <Drawer.Close />
            </Drawer.Header>
            <Drawer.Body>Overlay intensity: {intensity}</Drawer.Body>
          </Drawer.Panel>
        </Drawer>
      ))}
    </div>
  );
}

render(<OverlayIntensityDemo />);

Accessibility & Keyboard​

  • The drawer renders as a modal role="dialog" with proper aria-labelledby and aria-describedby relationships.
  • Focus is trapped inside the panel while open and restored on close.
  • Drawer.Title provides the accessible label; Drawer.Description provides the accessible description.
  • When dismissible={false}, pressing Escape and clicking outside will not close the drawer.
KeyBehavior
EscapeCloses the drawer (unless dismissible={false}).
Tab / Shift+TabCycles focus within the panel.

Animation​

The drawer animates automatically via CSS transitions. The panel slides in from the configured placement direction and the overlay fades in. No JavaScript animation library is required.

  • Panel: transform 0.3s with cubic-bezier(0.4, 0, 0.2, 1)
  • Overlay: opacity 0.3s with ease-in-out

API Reference​

Drawer​

Props​

NameTypeDefaultDescription
childrenReact.ReactNode-Drawer parts rendered inside the root.
forceMountbooleantrueKeep the overlay and panel mounted while closed so the exit transition can run. Pass false to unmount a heavy panel on close. The wrapper flips Spar's default.
placementDrawerPlacement'right'Side the drawer slides in from.
dismissiblebooleantrueWhether the drawer can be dismissed by clicking outside or pressing Escape.
idstring-Custom base ID for ARIA relationships. If not provided, one will be generated automatically. Sub-element IDs are derived as ${id}-title, ${id}-description, ${id}-content.
disabledbooleanfalseDisables all dialog triggers (prevents opening)
openboolean-Controlled open state
defaultOpenbooleanfalseInitial open state (uncontrolled)
modalbooleantrueWhether dialog is modal (blocks interaction outside)

Events​

NameTypeDefaultDescription
onOpenChange(open: boolean) => void-Callback when open state changes

Drawer.Trigger​

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Drawer.Overlay​

Props​

NameTypeDefaultDescription
invisiblebooleanfalseWhen true, the overlay is rendered but visually invisible.
intensityDrawerOverlayIntensity'base'Overlay backdrop intensity.
blurbooleanfalseApplies backdrop blur on the overlay.
classNamesPartial<Record<"root", string>>-
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-
containerHTMLElement | nulldocument.bodyPortal container element. Content is portaled to document.body by default.

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-stateAlways"open" or "closed" — used for entry/exit animations.
data-intensityAlwaysReflects the resolved intensity prop.
data-invisibleWhen invisible is trueIndicates overlay is visually hidden but still interactive.
data-blurWhen blur is trueEnables backdrop blur styling on the overlay.

Drawer.Panel​

Props​

NameTypeDefaultDescription
classNamesPartial<Record<"root", string>>-
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-
roleReact.AriaRole'dialog'ARIA role for dialog type
containerHTMLElement | nulldocument.bodyPortal container element. Content is portaled to document.body by default.
trapFocusbooleantrueEnable focus trapping
restoreFocusbooleantrueRestore focus on close
initialFocusHTMLElement | (() => HTMLElement)-Element to focus on open
finalFocusHTMLElement | (() => HTMLElement)-Element to focus on close

Events​

NameTypeDefaultDescription
onOpenAutoFocus(event: Event) => void-Callback before auto-focus
onCloseAutoFocus(event: Event) => void-Callback before focus restore
onEscapeKeyDown(event: KeyboardEvent) => void-Escape key handler
onPointerDownOutside(event: PointerEvent) => void-Outside click handler
onInteractOutside(event: PointerEvent) => void-Outside interaction handler with preventDefault capability

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-stateAlways"open" or "closed" — drives slide/scale animations.
data-placementAlwaysReflects placement for directional CSS transitions.

Drawer.Header​

Props​

NameTypeDefaultDescription
headerTypeDrawerHeaderType'basic'Type of the header.
classNamesPartial<Record<"root", string>>-
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-header-typeAlwaysReflects the resolved headerType prop.

Drawer.Title​

Props​

NameTypeDefaultDescription
classNamesPartial<Record<"root", string>>-
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-
levelnumber5Semantic heading level (1-6). Sets the rendered tag (h1-h6) and data-level for the document outline; the visual size is fixed regardless of level.

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-levelAlwaysReflects the resolved level prop for the document outline (does not change typography).

Drawer.Description​

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Drawer.Body​

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
NameTypeDefaultDescription
footerTypeDrawerFooterType'basic'Type of the footer.
classNamesPartial<Record<"root", string>>-
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-
AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-footer-typeAlwaysReflects the resolved footerType prop.

Drawer.Close​

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Type Definitions​

NameDefinition
DrawerPlacement'left' | 'right' | 'top' | 'bottom'
DrawerOverlayIntensity'lightest' | 'light' | 'base' | 'dark' | 'darkest'
DrawerHeaderType'basic' | 'divided' | 'light' | 'dark' | 'primary'
DrawerFooterType'basic' | 'divided' | 'light'