Skip to main content
Markdown

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.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.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', 'full-screen'].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.Close>
            </Drawer.Header>
            <Drawer.Body>
              This drawer slides in from the <strong>{p}</strong> side.
            </Drawer.Body>
          </Drawer.Panel>
        </Drawer>
      ))}
    </div>
  );
}

render(<PlacementDemo />);

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.Close>
            </Drawer.Header>
            <Drawer.Body>
              This footer uses <code>{type}</code> styling.
            </Drawer.Body>
            <Drawer.Footer footerType={type}>
              <Button variant="secondary">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.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.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.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.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.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
  • Full-screen: Scale + opacity transition instead of slide.

API Reference

Drawer

Props

NameTypeDefaultDescription
childrenReact.ReactNode-Drawer parts rendered inside the root.
placementDrawerPlacement-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)

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>>>-
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' | 'full-screen'
DrawerOverlayIntensity'lightest' | 'light' | 'base' | 'dark' | 'darkest'
DrawerHeaderType'basic' | 'divided' | 'light' | 'dark' | 'primary'
DrawerFooterType'basic' | 'divided' | 'light'