A shared React component library. Headless-friendly components built on Base UI, styled with Tailwind CSS v4 design tokens, and shipped as per-component subpaths so you only pull in what you use.
bun add @dirstack/ui
# or: npm install @dirstack/uiImport each component from its own subpath:
import { Button } from "@dirstack/ui/button"
import { Dialog, DialogContent, DialogTitle } from "@dirstack/ui/dialog"
import { cn, variants } from "@dirstack/ui/variants"
export function Example() {
return <Button variant="primary">Save</Button>
}The shared helpers live at fixed subpaths: @dirstack/ui/variants (cn, variants, VariantProps), @dirstack/ui/classes (shared class strings), @dirstack/ui/slot, @dirstack/ui/interactive (interactiveVariants), and @dirstack/ui/animation (ANIMATION_DURATION, ANIMATION_EASING — the shared motion timing, for syncing your own transitions with the library's).
Every file in src/components is its own subpath (@dirstack/ui/<name>):
- Layout:
page,stack,heading,section-header,card,detail-row,steps,nav,nav-button - Forms:
form,form-section,form-row,label,input,number-input,search-input,textarea,select,switch,copy-input - Actions & overlays:
button,copy-button,menu,popover,dialog,tooltip,tabs,toaster,kbd - Display & feedback:
badge,callout,empty-state,skeleton,ping(status dot with optional pulse),avatar,entity-avatar,pagination - Dashboard:
animated-number,chart,stat-strip,bar-list(ranked share bars,BarList.Row/BarList.Skeleton),breakdown-card(titled panel with optional tabs and footer),view-tabs(tab switcher bound to a?view=param),theme(useTheme+ThemeSelect)
The package ships its own theme. Import it once in your Tailwind v4 entry, after @import "tailwindcss":
@import "tailwindcss";
@import "@dirstack/ui/styles.css";That stylesheet carries the full design-token set (--color-* mirroring the shadcn/ui variable names plus success/warning/danger trios, --font-*, --spacing-*, keyframes), the tailwindcss-animate plugin, and a @source directive that scans the library so its utility classes are generated for you. You do not need to add the package to your own @source list.
Every color is a light-dark() pair driven by color-scheme: left alone it follows the OS, and data-theme="dark" or data-theme="light" on <html> forces one. The theme expects the consumer to load the "Inter Variable" font; without it, type falls back to the system stack.
@dirstack/ui/theme manages that attribute for you: useTheme() returns [theme, setTheme] ("system" | "light" | "dark"), persists the choice in localStorage.theme, keeps tabs in sync, and ThemeSelect is a ready-made segmented System / Light / Dark control. The hook applies the stored theme once it mounts; to avoid a flash of the wrong scheme, apply it before first paint with this inline script in <head>:
<script>try{var t=localStorage.theme;if(t==="light"||t==="dark")document.documentElement.dataset.theme=t}catch(e){}</script>Override any token in your own CSS to re-theme:
@theme {
--color-primary: light-dark(oklch(0.6 0.2 250), oklch(0.7 0.18 250));
}react>= 19 andreact-dom>= 19@base-ui/react>= 1react-hook-form>= 7 (only for the form components)tailwindcss>= 4tailwindcss-animate>= 1
bun install
bun run typecheck
bun run build # tsdown: per-module JS + d.ts, plus dist/styles.cssMIT © Piotr Kulpinski