hiraku (開く, "to open") - Strongly typed, modal state management system for React (Radix UI + Base UI)
Features • Packages • Installation • Quick Start • Development • Why hiraku?
- ⚡ 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, seeMIGRATION.md
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/) |
Most apps should install an integration package:
npm install @hirotoshioi/hiraku-radix-uiRadix UI dialog primitives are required as peer dependencies:
npm install @radix-ui/react-dialog @radix-ui/react-alert-dialognpm install @hirotoshioi/hiraku-base-uiBase UI is required as a peer dependency:
npm install @base-ui/reactThis section uses the Radix UI integration. For Base UI, see packages/base-ui/README.md.
// app.tsx
import { ModalProvider } from "@hirotoshioi/hiraku-radix-ui";
function App() {
return (
<>
<YourApp />
<ModalProvider/>
</>
);
}// 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>();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
}
}Example apps live in this repository:
examples/radix-ui/examples/base-ui/
Or try the (separate) Radix UI example repo 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
Run from repository root:
npm run build
npm run dev
npm test
npm run typecheck
npm run lintWith 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 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>
</>
);
}MIT © Hiroto Shioi