Back to Blog

Using Ark UI with Storybook

How to set up Storybook with Ark UI — writing stories for compound components, styling, and prop documentation

Sage

/

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.

Using Ark UI with Storybook | Ark UI