A sliding paper panel opening on a wooden track with a calm gray cat watching nearby.

Hiraki: Slide-In Drawers Without the Dependency Hangover

The Gray Cat
The Gray Cat
0 views

If you have ever wired up a bottom sheet that flicks closed when you fling it downward, snaps to a comfortable middle height, and rubber-bands gently when you over-drag it, you know that the magic is also the heavy part. Most React drawers earn that feel by leaning on a stack of dependencies: a dialog primitive for accessibility, a gesture library for the dragging, an animation runtime for the spring. Hiraki takes the opposite bet. It is a headless, unstyled drawer primitive that reproduces that iOS-sheet experience with zero runtime dependencies and a footprint of roughly 10 KB gzipped. React 18 or newer is the only thing it asks for.

The name comes from the Japanese 開き, meaning "opening," and the library leans into that idea: drawers that slide in from any of the four edges, snap to points you define, and respond to the velocity of your gesture rather than just where your finger lands. If you have used Vaul or any Radix-based dialog, the API will feel immediately familiar. The difference is what is underneath, which is to say, almost nothing. Before we go further, a fair warning that we will repeat at the end: Hiraki is genuinely new. At the time of writing it sits at version 0.0.7, was published in March 2026 by a single author, and has very modest adoption. It is a promising design worth knowing about, not a battle-tested production fixture. Pin your version and read on with that lens.

What Makes It Tick

Hiraki bundles a surprising amount of behavior into its small size. The headline features:

  • All four directions. Drawers can slide from top, bottom, left, or right, not just up from the bottom edge. Side panels and top notification sheets come for free.
  • Velocity-aware gestures. The library tracks drag velocity with exponential smoothing, so a quick flick dismisses the drawer even if you have not dragged it past the halfway mark, exactly the way native mobile sheets behave.
  • Snap points. Define resting positions as pixels, percentages of the viewport, or the natural height of your content, and let the gesture decide which one to land on.
  • Six layout variants. Out of the box you get default, floating, sheet, fullscreen, nested, and stack, so the same primitive covers a docked sheet and a floating card.
  • Pure-CSS animation. Transitions run on CSS rather than a JavaScript animation loop, which is a big part of how the bundle stays small.
  • Headless and unstyled. No CSS file ships, and no class names are imposed. You bring Tailwind, CSS variables, or inline styles.
  • Comfort details built in. Focus trapping, scroll locking, an iOS Safari scroll fix, optional rubber-band overscroll, and inertia on release are all handled for you.

That combination, zero dependencies plus four directions plus real gestures and snap points in around 10 KB, is the niche Hiraki carves out. Most alternatives give you one or two of those, but not all of them at once.

Getting It Into Your Project

Installation is a single package with no transitive dependencies to vet.

npm install hiraki

Or with Yarn:

yarn add hiraki

The only peer dependencies are react and react-dom at version 18 or higher. The package ships as dual ESM and CJS with bundled TypeScript declarations, and it is marked side-effect free, so tree-shaking works as expected. One small note for later: the package.json advertises a hiraki/styles.css export, but no stylesheet actually ships in the build. Do not try to import it. Hiraki is genuinely headless, and the CSS export entry is vestigial.

Your First Sheet

Hiraki uses a compound component pattern. A Drawer object exposes a set of parts that you compose together, each forwarding refs and standard HTML attributes like className, style, and event handlers. Here is a complete bottom sheet:

import { Drawer } from 'hiraki';

function ProfileSheet() {
  return (
    <Drawer.Root>
      <Drawer.Trigger className="rounded-md bg-black px-4 py-2 text-white">
        Open profile
      </Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Overlay className="fixed inset-0 bg-black/40" />
        <Drawer.Content className="fixed inset-x-0 bottom-0 rounded-t-2xl bg-white p-6 shadow-xl">
          <Drawer.Handle className="mx-auto mb-4 h-1.5 w-12 rounded-full bg-gray-300" />
          <Drawer.Title className="text-lg font-semibold">Your profile</Drawer.Title>
          <Drawer.Description className="text-sm text-gray-500">
            Drag down to dismiss, or flick it away.
          </Drawer.Description>
          <Drawer.Close className="mt-6 text-sm text-gray-400">Close</Drawer.Close>
        </Drawer.Content>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

Drawer.Root owns the state and provides context. Drawer.Trigger opens the sheet, Drawer.Portal renders the content into document.body (or a custom container you pass), Drawer.Overlay is the backdrop, and Drawer.Content is the panel that slides. Drawer.Handle gives users a grabber to drag, while Drawer.Title and Drawer.Description exist for accessible labelling. Anyone coming from Vaul or Radix Dialog will recognize this shape instantly, which makes Hiraki easy to adopt and easy to migrate to.

Driving It From State

By default the drawer manages its own open state, but you can lift that into your own component whenever you need to open it programmatically or react to its changes.

import { useState } from 'react';
import { Drawer } from 'hiraki';

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

  return (
    <>
      <button onClick={() => setOpen(true)}>Open from anywhere</button>
      <Drawer.Root open={open} onOpenChange={setOpen}>
        <Drawer.Portal>
          <Drawer.Overlay className="fixed inset-0 bg-black/40" />
          <Drawer.Content className="fixed inset-x-0 bottom-0 bg-white p-6">
            <Drawer.Title>Controlled</Drawer.Title>
          </Drawer.Content>
        </Drawer.Portal>
      </Drawer.Root>
    </>
  );
}

The open and onOpenChange pair follows the same controlled-component convention you already use across the React ecosystem, so it composes cleanly with whatever state management you prefer.

Picking a Direction

Switching which edge the drawer enters from is a single prop. Want a navigation panel that slides in from the right? Set direction="right" and adjust your positioning styles to match.

<Drawer.Root direction="right">
  <Drawer.Trigger>Menu</Drawer.Trigger>
  <Drawer.Portal>
    <Drawer.Overlay className="fixed inset-0 bg-black/40" />
    <Drawer.Content className="fixed inset-y-0 right-0 w-80 bg-white p-6">
      <Drawer.Title>Navigation</Drawer.Title>
    </Drawer.Content>
  </Drawer.Portal>
</Drawer.Root>

Because Hiraki is headless, you own the layout. The library handles the gesture math, the snap behavior, and the slide animation for the direction you chose, and the default variant even rounds only the two corners that face the open direction so the panel reads correctly without extra work.

Going Deeper

Snap Points That Feel Native

Snap points are where the drawer starts to feel like a real mobile sheet. You pass an array of resting positions, and Hiraki figures out which one a gesture should land on based on both position and velocity.

<Drawer.Root snapPoints={['25%', '55%', '90%']}>
  <Drawer.Trigger>Open details</Drawer.Trigger>
  <Drawer.Portal>
    <Drawer.Overlay className="fixed inset-0 bg-black/40" />
    <Drawer.Content className="fixed inset-x-0 bottom-0 bg-white">
      <Drawer.Handle />
      <Drawer.SnapIndicator />
      <Drawer.ScrollArea className="max-h-full overflow-y-auto p-6">
        {/* long content here */}
      </Drawer.ScrollArea>
    </Drawer.Content>
  </Drawer.Portal>
</Drawer.Root>

Snap points accept three formats. A raw number like 200 means 200 pixels of the drawer are visible. A percentage string like '55%' is measured against the viewport height (or width for left and right drawers). And the special 'content' value snaps to the drawer's own natural content height. The drawer opens at the largest snap point and lets the user pull down to smaller ones.

Two parts in that example earn their keep with snap points. Drawer.SnapIndicator renders a visual cue for the current resting position, and Drawer.ScrollArea is a scrollable region that coordinates with the drag gesture, so scrolling the content does not accidentally start dragging the sheet, and reaching the top of the scroll hands control back to the drawer. If you need full control, the activeSnapPoint and onSnapPointChange props let you drive the active index from your own state, the same way open and onOpenChange work for visibility.

Reading the Gesture in Real Time

For richer interactions, Hiraki exposes the gesture stream through callbacks. onDragStart, onDrag, and onDragEnd each receive a structured payload describing what the user's finger is doing.

import { Drawer, type GestureCallbackData } from 'hiraki';

function GestureAwareSheet() {
  function handleDrag(data: GestureCallbackData) {
    console.log(data.velocity, data.translateValue, data.direction);
  }

  return (
    <Drawer.Root onDrag={handleDrag} closeThreshold={0.4}>
      <Drawer.Trigger>Open</Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Content className="fixed inset-x-0 bottom-0 bg-white p-6">
          <Drawer.Title>Interactive</Drawer.Title>
        </Drawer.Content>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

The GestureCallbackData object carries the current velocity, the active direction, the translateValue in pixels, and the resolved snapPoint when one applies. You might use velocity to trigger haptic feedback on a flick, or translateValue to fade in a secondary element as the sheet rises. The closeThreshold prop, a fraction between 0 and 1, controls how far the drawer must be dragged before releasing it auto-closes, giving you a second lever alongside the velocity logic.

Pure-CSS Visual Effects

Because the animation runs on CSS, Hiraki hands you a custom property you can hook into without any JavaScript. While a drag is in progress, --hiraki-drag-progress updates every frame from 0 (closed) to 1 (fully open). Bind anything you like to it.

.drawer-content {
  /* dim a backdrop or scale a hero as the sheet opens */
  opacity: calc(0.5 + var(--hiraki-drag-progress, 0) * 0.5);
}

For the popular "page shrinks behind the sheet" effect, set shouldScaleBackground on the root and mark the element you want scaled with a data attribute.

<Drawer.Root shouldScaleBackground>
  {/* drawer parts */}
</Drawer.Root>

<div data-hiraki-background>
  {/* the rest of your page */}
</div>

This is a tidy demonstration of the library's philosophy. Instead of shipping an animation engine, it exposes the raw drag progress and lets the platform do the rendering, which keeps the bundle small and the effects fully in your control.

How It Stacks Up

The obvious comparison is Vaul, the widely used drawer built on Radix Dialog that powers the drawer in shadcn/ui. Vaul is mature, battle-tested, and downloaded millions of times a week, but it brings Radix along for the ride. Other options like react-modal-sheet lean on Framer Motion, and react-spring-bottom-sheet depends on react-spring. Each is a fine choice, and each adds a runtime dependency. Hiraki's distinct position is being the one that combines zero dependencies, all four directions, and gesture-plus-snap support in roughly 10 KB. For a bundle-conscious team that wants the iOS-sheet feel without a dependency tree to audit, that is a genuinely appealing trade.

The Honest Caveat

It would be unfair to leave you without the maturity check. Hiraki is at version 0.0.7. Every release so far landed within about thirteen hours across two days in March 2026, and there has been no further activity since. It has a single maintainer, a few dozen GitHub stars, and very low weekly download numbers. The design is thoughtful and the API is pleasant, but this is a fresh side project, not a hardened library with years of edge cases shaken out. Pre-1.0 software can change its API without ceremony, so if you adopt Hiraki, pin the exact version, watch the repository for signs of life, and keep a more established option like Vaul in mind for anything where you cannot afford surprises.

Closing Thoughts

Hiraki is a lovely proof that you do not need a stack of dependencies to get a polished, gesture-driven drawer. Four directions, velocity-aware dismissal, flexible snap points, six variants, and a clean Radix-style compound API, all in a headless package that weighs about as much as a small image. The familiar API means you can be productive in minutes, and the pure-CSS approach gives you escape hatches like --hiraki-drag-progress that more opaque libraries hide. Just go in with clear eyes about its youth. As a "one to watch," it is well worth a spot in your bookmarks, and for a side project or an internal tool where you can pin the version, it might be exactly the lightweight drawer you were hoping someone would build.