Storybook is a great way to develop and document your design system in isolation. Ark UI works well with it — you just need to know how the pieces fit together. Here's a walkthrough of setting up Storybook with Ark UI React.
Installation
Install Storybook alongside your Ark UI project:
npx storybook@latest init
This sets up Storybook with @storybook/react-vite and creates a .storybook directory with main.ts and
preview.ts.
Writing Stories
Ark UI uses compound components — Checkbox.Root, Checkbox.Control, Checkbox.Label, etc. Each story composes these
parts together, just like you would in your app:
import type { Meta, StoryObj } from '@storybook/react-vite'
import { Checkbox, CheckboxRoot } from '@ark-ui/react/checkbox'
import { CheckIcon } from 'lucide-react'
const meta: Meta<typeof CheckboxRoot> = {
title: 'Checkbox',
component: CheckboxRoot,
}
export default meta
type Story = StoryObj<typeof CheckboxRoot>
export const Basic: Story = {
render: (args) => (
<Checkbox.Root {...args}>
<Checkbox.Control>
<Checkbox.Indicator>
<CheckIcon />
</Checkbox.Indicator>
</Checkbox.Control>
<Checkbox.Label>Accept terms</Checkbox.Label>
<Checkbox.HiddenInput />
</Checkbox.Root>
),
}
This renders a fully functional, accessible checkbox. No state management needed — the @zag-js state machine handles
that internally.
Styling
Ark UI is headless, so you bring your own styles. CSS modules work well with Storybook:
import type { Meta, StoryObj } from '@storybook/react-vite'
import { Switch, SwitchRoot } from '@ark-ui/react/switch'
import styles from './switch.module.css'
const meta: Meta<typeof SwitchRoot> = {
title: 'Switch',
component: SwitchRoot,
}
export default meta
type Story = StoryObj<typeof SwitchRoot>
export const Basic: Story = {
render: (args) => (
<Switch.Root {...args} className={styles.Root}>
<Switch.Control className={styles.Control}>
<Switch.Thumb className={styles.Thumb} />
</Switch.Control>
<Switch.Label className={styles.Label}>Dark mode</Switch.Label>
<Switch.HiddenInput />
</Switch.Root>
),
}
Ark UI exposes data-state and data-disabled attributes on every part, so you can style states purely in CSS:
.Control {
background: var(--color-neutral);
border-radius: 999px;
transition: background 150ms ease;
&[data-state='checked'] {
background: var(--color-primary);
}
&[data-disabled] {
opacity: 0.5;
}
}
No need to wire up JavaScript for visual states.
Prop Documentation with Controls
By default, Storybook won't pick up Ark UI's component props. This is because the props flow through @zag-js types in
node_modules, and Storybook's docgen filters those out. To fix this, you need three things.
1. Enable react-docgen-typescript
In .storybook/main.ts, enable docgen with a propFilter that allows @zag-js types through:
const config: StorybookConfig = {
// ...
typescript: {
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
shouldRemoveUndefinedFromOptional: true,
propFilter: (prop) => {
if (prop.declarations && prop.declarations.length > 0) {
return prop.declarations.some(
(declaration) => !declaration.fileName.includes('node_modules') || declaration.fileName.includes('@zag-js'),
)
}
return true
},
},
},
}
2. Set component and use the args pattern
Point Storybook at the Root component so it knows what to analyze, and spread args onto it so the Controls panel
renders. This is the same story format from the earlier examples:
import type { Meta, StoryObj } from '@storybook/react-vite'
import { Checkbox, CheckboxRoot } from '@ark-ui/react/checkbox'
const meta: Meta<typeof CheckboxRoot> = {
title: 'Checkbox',
component: CheckboxRoot, // tells docgen what to analyze
}
export default meta
type Story = StoryObj<typeof CheckboxRoot>
export const Basic: Story = {
render: (args) => (
<Checkbox.Root {...args}>
{' '}
{/* spread args to connect controls */}
{/* ... */}
</Checkbox.Root>
),
}
With both in place, Storybook will display the full props table — checked, disabled, readOnly, onCheckedChange,
and everything else — with descriptions and types.
Wrapping Up
Ark UI's compound component pattern maps naturally to Storybook stories. Write your stories the same way you'd use the components in your app, add CSS modules for styling, and enable docgen if you want prop documentation. That's all there is to it.