overview
Accessible drawer behavior, none of the visual system.
hiraki handles semantics, gestures, snap points, dismissal, and focus management. Your product keeps full control of the surface — color, radius, type, spacing, and motion.
It ships zero runtime dependencies: no Radix, no Framer Motion, ~10 KB gzipped. Behavior lives in the library; everything you can see stays in your app.
There is no CSS file to import and no class names to override. You style the primitives with Tailwind, CSS variables, or the style prop — and the same component can look calm and product-like in one place and loud and editorial in another.
install
Add the package
Install with your package manager of choice, then compose the primitives directly in your UI.
pnpm add hirakinpm install hirakiquick start
Structure first, styling second
Start from the structural parts. Once the behavior works, attach your classes and tokens.
import { Drawer } from 'hiraki'
export function Example() {
return (
<Drawer.Root>
<Drawer.Trigger className="trigger">Open</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay className="overlay" />
<Drawer.Content className="content">
<Drawer.Handle className="handle" />
<Drawer.Title className="title">Edit profile</Drawer.Title>
<Drawer.Description className="description">
Build your own design around the primitives.
</Drawer.Description>
<Drawer.Close className="close">Close</Drawer.Close>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
)
}Open it to feel the gesture, velocity, and dismissal in a minimal product skin:
styling
One primitive, any product voice
The behavior layer stays stable while your team owns the visual direction. Both drawers below are the same primitives — only the class names differ.
// Same primitive, swap the classes.
<Drawer.Content className="rounded-t-2xl bg-white text-zinc-900">
{/* soft product sheet */}
</Drawer.Content>
<Drawer.Content className="rounded-none border-l-2 border-white bg-neutral-950">
{/* square editorial panel */}
</Drawer.Content>behavior
Behavior stays consistent while the style changes
Direction, snap points, dismissal, and drag physics come from the primitive layer.
- Directions — bottom, top, left, and right, each with the correct gesture axis.
- Snap points — pixel values, percentages, and the
contentkeyword. - Accessibility — dialog semantics, focus trap, Escape, overlay dismissal, scroll lock.
- Control model — open state and active snap point can both be controlled from the parent.
- Handle — an optional affordance; place it where the direction makes sense, or hide it.
<Drawer.Root
snapPoints={['25%', '55%', '90%']}
activeSnapPoint={snap}
onSnapPointChange={setSnap}
direction="bottom"
dismissible
>
<Drawer.Portal>
<Drawer.Overlay className="overlay" />
<Drawer.Content className="content">
<Drawer.Handle visible />
<Drawer.Title>Filters</Drawer.Title>
<Drawer.SnapIndicator />
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>api
Compound API
Every prop, type, and default. All parts also forward standard HTML attributes — className, style, data-*, and event handlers.
Drawer.Root
open—boolean
—Controlled open state. Pair with onOpenChange.
defaultOpenfalseboolean
falseUncontrolled initial open state.
onOpenChange—(open: boolean) => void
—Called whenever the drawer opens or closes.
direction"bottom""top" | "bottom" | "left" | "right"
"bottom"Which edge the drawer slides from. Determines gesture axis and transform direction.
variant"default""default" | "floating" | "sheet" | "fullscreen" | "nested" | "stack"
"default"Visual mode. Affects border radius, margin, and animation preset.
modaltrueboolean
trueLocks page scroll and traps focus inside the drawer while open.
dismissibletrueboolean
trueWhether overlay click and Escape key close the drawer.
snapPoints[]SnapPoint[]
[]Snap positions. number = px from edge, "25%" = fraction of viewport, "content" = content height.
activeSnapPoint—number
—Controlled snap index. 0 is the smallest point, length - 1 is the largest.
onSnapPointChange—(index: number) => void
—Called when the active snap index changes after a drag.
closeThreshold0.5number
0.5Fraction of the drawer height that must be dragged before auto-close triggers on release.
rubberBandtrueboolean
trueElastic resistance when dragging past the open or closed extent.
inertiatrueboolean
trueUses release velocity to project the landing snap point. Fast flick = skip a snap.
shouldScaleBackgroundfalseboolean
falseScales the page element behind the drawer down when open. Add data-hiraki-background to the element to scale.
onDragStart—(data: GestureCallbackData) => void
—Fires once when a drag gesture is confirmed (after axis lock).
onDrag—(data: GestureCallbackData) => void
—Fires on every pointer move during a drag.
onDragEnd—(data: GestureCallbackData) => void
—Fires when the pointer is released, with final velocity and translate.
Drawer.Portal
containerdocument.bodyHTMLElement | null
document.bodyDOM node to portal into. Defaults to document.body.
Drawer.Trigger
asChildfalseboolean
falseMerges props onto the child element instead of rendering a button.
Drawer.Close
asChildfalseboolean
falseMerges props onto the child element instead of rendering a button.
Drawer.Handle
visibletrueboolean
trueControls whether the handle pill is rendered.
handleOnlyfalseboolean
falseWhen true, drag gestures only start from the handle, not the full content area.
Drawer.Content
...HTMLAttributes—React.HTMLAttributes<HTMLDivElement>
—All standard div props forwarded. Use className to apply your own styles.
Drawer.Overlay
...HTMLAttributes—React.HTMLAttributes<HTMLDivElement>
—All standard div props forwarded. Opacity tracks drag progress via direct DOM mutation.