Skip to Content
Hashti Design System
Components

Drawer

Sliding panel originating from any screen edge, ideal for deep filters, side navigation, or details inspect.

Drawer

The Drawer component provides a smooth sliding overlay panel that emerges from any screen boundary (left, right, top, bottom, or direction-aware start and end). It is the optimal pattern for detailed object inspections, extensive multi-step filter forms, auxiliary navigation menus, and mobile bottom sheets.

In classical Persian spatial design, the Drawer mirrors the concept of an arched alcove or side chamber (shah-neshin)—providing a rich, deep spatial annex that expands smoothly alongside the main hall without dislodging the user’s primary context.

DrawerPerimeter inspection panel sliding in from screen edge.

Import

import { Drawer, Button } from "@hashti/react";

Examples

Right-Edge Sliding Drawer (Default)

By default, Drawer slides out from the trailing edge (placement="end"), including built-in header title, description, close button, and footer actions:

import { useState } from "react";
import { Drawer, Button, TextField } from "@hashti/react";

export function RightDrawerDemo() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <Button variant="primary" onPress={() => setIsOpen(true)}>
        Inspect Architectural Spec
      </Button>

      <Drawer
        isOpen={isOpen}
        onOpenChange={setIsOpen}
        title="Sheikh Lotfollah Mosque Dome"
        description="Structural and decorative specifications of the peacock dome."
        footer={
          <div className="flex justify-end gap-3 w-full">
            <Button variant="ghost" onPress={() => setIsOpen(false)}>
              Close
            </Button>
            <Button variant="primary" onPress={() => setIsOpen(false)}>
              Export Dossier
            </Button>
          </div>
        }
      >
        <div className="flex flex-col gap-4 text-sm text-gray-700 dark:text-gray-300">
          <p>
            Constructed between 1603 and 1619 during the reign of Shah Abbas I, the dome features a
            delicate transition from an exterior octagonal base to an elevated single-shell vault.
          </p>
          <TextField label="Restoration Lead Annotations" placeholder="Add field notes..." />
        </div>
      </Drawer>
    </>
  );
}

Left-Edge Sidebar Navigation

Use placement="left" (or start in LTR) for side navigation drawers:

import { useState } from "react";
import { Drawer, Button } from "@hashti/react";

export function LeftSidebarDrawerDemo() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <Button variant="secondary" onPress={() => setIsOpen(true)}>
        Open Navigation Drawer
      </Button>

      <Drawer
        placement="left"
        size="sm"
        isOpen={isOpen}
        onOpenChange={setIsOpen}
        title="Project Navigation"
      >
        <nav className="flex flex-col gap-2 py-2">
          <a href="#overview" className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800">
            Overview & Telemetry
          </a>
          <a href="#masonry" className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800">
            Brickwork & Mortar
          </a>
          <a href="#tiling" className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800">
            Glazed Haft-Rangi Tiles
          </a>
          <a href="#vaults" className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800">
            Vault Calculations
          </a>
        </nav>
      </Drawer>
    </>
  );
}

Mobile Bottom-Sheet Drawer

Set placement="bottom" to render a mobile bottom-sheet style drawer:

import { useState } from "react";
import { Drawer, Button } from "@hashti/react";

export function BottomSheetDemo() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <Button variant="secondary" onPress={() => setIsOpen(true)}>
        Open Mobile Action Sheet
      </Button>

      <Drawer
        placement="bottom"
        size="md"
        isOpen={isOpen}
        onOpenChange={setIsOpen}
        title="Actions"
        description="Select an operation for the highlighted survey segment."
      >
        <div className="grid grid-cols-2 gap-3 py-3">
          <Button variant="secondary">Measure Acoustic Reverberation</Button>
          <Button variant="secondary">Capture 3D Photogrammetry</Button>
          <Button variant="secondary">Inspect Moisture Levels</Button>
          <Button variant="primary">Generate Structural Report</Button>
        </div>
      </Drawer>
    </>
  );
}

Size Variants (sm, md, lg, xl, full)

Drawer supports five standardized sizing tiers:

  • sm: 320px width (compact navigation or inspection panels)
  • md: 480px width (default, standard forms and record cards)
  • lg: 640px width (complex forms with multi-column inputs)
  • xl: 800px width (code editors and CAD blueprint inspects)
  • full: 100% viewport width (full-screen immersive workflows)
import { useState } from "react";
import { Drawer, Button, type DrawerSize } from "@hashti/react";

export function DrawerSizesDemo() {
  const [activeSize, setActiveSize] = useState<DrawerSize | null>(null);

  return (
    <div className="flex flex-wrap gap-2">
      {(["sm", "md", "lg", "xl", "full"] as DrawerSize[]).map((size) => (
        <Button key={size} variant="secondary" onPress={() => setActiveSize(size)}>
          Size: {size.toUpperCase()}
        </Button>
      ))}

      {activeSize && (
        <Drawer
          size={activeSize}
          isOpen={activeSize !== null}
          onOpenChange={(open) => !open && setActiveSize(null)}
          title={`Drawer Dimension: ${activeSize.toUpperCase()}`}
          description={`Sliding panel demonstrating the '${activeSize}' dimension preset.`}
        >
          <p className="text-sm">
            This drawer is styled with the <code>{activeSize}</code> size constraint.
          </p>
        </Drawer>
      )}
    </div>
  );
}

Bilingual & RTL Direction-Awareness

When using placement="start" or placement="end", the drawer automatically adapts to the reading direction:

  • In LTR, "end" slides from the right; "start" slides from the left.
  • In RTL (Persian/Arabic), "end" slides from the left; "start" slides from the right.
import { useState } from "react";
import { Drawer, Button } from "@hashti/react";

export function RTLDrawerDemo() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div dir="rtl" className="font-sans">
      <Button variant="primary" onPress={() => setIsOpen(true)}>
        مشاهده جزئیات ایوان
      </Button>

      <Drawer
        dir="rtl"
        placement="end"
        isOpen={isOpen}
        onOpenChange={setIsOpen}
        title="مشخصات معماری ایوان اصلی"
        description="بررسی مقرنس‌ها و قوس جناغی ورودی عمارت."
      >
        <p className="text-sm text-gray-700 dark:text-gray-300">
          ایوان ورودی دارای دهانه‌ای به عرض ۱۲ متر و ارتفاع ۱۸ متر می‌باشد.
        </p>
      </Drawer>
    </div>
  );
}

Props Reference

PropTypeDefaultDescription
isOpenboolean—Controlled open visibility state.
defaultOpenbooleanfalseUncontrolled initial open state.
onOpenChange(isOpen: boolean) => void—Handler invoked when open/close state transitions.
placement'left' | 'right' | 'top' | 'bottom' | 'start' | 'end''end'Edge of the viewport where the drawer originates.
size'sm' | 'md' | 'lg' | 'xl' | 'full''md'Width (or height for top/bottom) dimension preset.
titleReactNode—Header title rendered inside <Heading slot="title">.
descriptionReactNode—Optional secondary description text beneath the title.
footerReactNode—Actions or buttons pinned to the bottom of the drawer.
isDismissablebooleantrueAllows closing by clicking the backdrop or pressing Esc.
dir'ltr' | 'rtl'—Text direction override for bilingual layouts.
childrenReactNode—Primary scrollable content rendered in the drawer body.

Accessibility

  • Focus Trap & Traversal: While open, keyboard focus is strictly contained within the drawer. Tabbing loops from the header close button through the body content to the footer actions.
  • Escape Key Dismissal: Pressing Esc immediately dismisses the drawer and returns focus to the initiating button.
  • Semantic Dialog Container: Internally renders <RACDialog role="dialog"> with automated aria-labelledby referencing the drawer’s title heading.
  • Motion Reduction: Slide transitions and backdrop fades automatically disable or collapse into instantaneous cuts when the user has enabled prefers-reduced-motion.

Keyboard Interactions

KeyAction
TabCycles focus within the drawer slideout container.
EscCloses drawer panel and restores focus to trigger.

WAI-ARIA & Data Attributes

AttributeTargetDescription
[data-entering]Drawer SurfacePresent during slide-in animation.
[data-exiting]Drawer SurfacePresent during slide-out animation.