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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
isOpen | boolean | — | Controlled open visibility state. |
defaultOpen | boolean | false | Uncontrolled 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. |
title | ReactNode | — | Header title rendered inside <Heading slot="title">. |
description | ReactNode | — | Optional secondary description text beneath the title. |
footer | ReactNode | — | Actions or buttons pinned to the bottom of the drawer. |
isDismissable | boolean | true | Allows closing by clicking the backdrop or pressing Esc. |
dir | 'ltr' | 'rtl' | — | Text direction override for bilingual layouts. |
children | ReactNode | — | 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 automatedaria-labelledbyreferencing 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
| Key | Action |
|---|---|
| Tab | Cycles focus within the drawer slideout container. |
| Esc | Closes drawer panel and restores focus to trigger. |
WAI-ARIA & Data Attributes
| Attribute | Target | Description |
|---|---|---|
[data-entering] | Drawer Surface | Present during slide-in animation. |
[data-exiting] | Drawer Surface | Present during slide-out animation. |