Skip to content

HirotoShioi/hiraku

Repository files navigation

hiraku

hiraku (開く, "to open") - Strongly typed, modal state management system for React (Radix UI + Base UI)

npm version (radix-ui) npm version (base-ui) npm version (core) license Ask DeepWiki

FeaturesPackagesInstallationQuick StartDevelopmentWhy hiraku?


Features

  • Open from anywhere - Call modal.open() from any file, even outside React components
  • 🔒 Type-safe - Strongly typed props + close results
  • 🧩 Multiple UI frameworks - Radix UI and Base UI providers (same core API)
  • 🪶 Lightweight - zustand-based core
  • 🎨 shadcn/ui ready - Works seamlessly with your existing components
  • 😃 Migrate with ease - If you used @hirotoshioi/hiraku, see MIGRATION.md

Packages

This repository is a monorepo:

Package Purpose
@hirotoshioi/hiraku-radix-ui Radix UI integration (packages/radix-ui/)
@hirotoshioi/hiraku-base-ui Base UI integration (packages/base-ui/)
@hirotoshioi/hiraku-core Shared logic & types (packages/core/)
@hirotoshioi/hiraku Deprecated alias (re-exports Radix UI) (packages/hiraku/)

Installation

Most apps should install an integration package:

Radix UI

npm install @hirotoshioi/hiraku-radix-ui

Radix UI dialog primitives are required as peer dependencies:

npm install @radix-ui/react-dialog @radix-ui/react-alert-dialog

Base UI

npm install @hirotoshioi/hiraku-base-ui

Base UI is required as a peer dependency:

npm install @base-ui/react

Quick Start

This section uses the Radix UI integration. For Base UI, see packages/base-ui/README.md.

1. Add the Provider

// app.tsx
import { ModalProvider } from "@hirotoshioi/hiraku-radix-ui";

function App() {
  return (
    <>
      <YourApp />
      <ModalProvider/>
    </>
  );
}

2. Create a modal

// modals/confirm-dialog.tsx
import { DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { createDialog } from "@hirotoshioi/hiraku-radix-ui";

interface ConfirmDialogProps {
  title: string;
  message: string;
}

// No need to wrap with Dialog.Root, hiraku will take care of it
function ConfirmDialog({ title, message }: ConfirmDialogProps) {
  return (
    <DialogContent>
      <DialogHeader>
        <DialogTitle>{title}</DialogTitle>
      </DialogHeader>
      <p>{message}</p>
      <DialogFooter>
        <Button variant="outline" onClick={() => confirmDialog.close({ role: "cancel" })}>
          Cancel
        </Button>
        <Button onClick={() => confirmDialog.close({ data: true, role: "confirm" })}>
          Confirm
        </Button>
      </DialogFooter>
    </DialogContent>
  );
}

// Create a modal controller
export const confirmDialog = createDialog(ConfirmDialog).returns<boolean>();

3. Open from anywhere

import { confirmDialog } from "./modals/confirm-dialog";

async function handleDelete() {
  // Open the modal and wait for it
  await confirmDialog.open({
    title: "Delete item?",
    message: "This action cannot be undone.",
  });

  const { data, role } = await confirmDialog.onDidClose();

  if (role === "confirm" && data) {
    // Perform delete
  }
}

Examples

Example apps live in this repository:

  • examples/radix-ui/
  • examples/base-ui/

Or try the (separate) Radix UI example repo in StackBlitz:

Open in StackBlitz

For full package-specific docs:

  • Radix UI: packages/radix-ui/README.md
  • Base UI: packages/base-ui/README.md
  • Core: packages/core/README.md

Development

Run from repository root:

npm run build
npm run dev
npm test
npm run typecheck
npm run lint

Why hiraku?

With traditional patterns, modal components are often controlled by their parent for open/close state. That tight coupling hurts readability and maintainability.

If you’ve built React apps, you’ve probably seen something like this:

import { MyDialog } from "./MyDialog";
function Parent() {
  // Managing modal state in the parent makes the code cumbersome
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open Dialog</button>
      {/* The Dialog has to receive isOpen from the parent */}
      <MyDialog isOpen={isOpen} onClose={() => setIsOpen(false)} />
    </>
  );
}

The modal wants its open/close state managed by the parent, but doing so makes the parent code cumbersome.

hiraku's Approach

hiraku resolves that dilemma. With hiraku, modals can be opened from anywhere in your application without needing to pass down state or handlers through props. This decouples modal logic from your component hierarchy, leading to cleaner and more maintainable code.

import { myDialog } from "./modals/my-dialog";

function Parent() {
  // const [isOpen, setIsOpen] = useState(false); <-- No need to manage state!
  return (
    <>
      <button onClick={() => myDialog.open()}>
        Open Dialog
      </button>
    </>
  );
}

License

MIT © Hiroto Shioi

About

Strongly typed, modal state management system for Radix UI

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors