Storybook
Overview
Styleframe integrates with Storybook so you can develop, document, and test your design system components in isolation. The Styleframe Vite plugin handles all the wiring — your design tokens, utility classes, and recipe functions are available in every story automatically.
Key features:
- Zero-config styling: Import
virtual:styleframe.cssonce and all tokens, utilities, and recipes are available - Type-safe recipes: Component stories get full autocompletion for recipe variant props
- Co-located styles: Each component's
*.styleframe.tsfile is discovered and compiled automatically - Dark mode support: Toggle themes in the Storybook UI with
data-themeattributes - Framework-agnostic tokens: The same
styleframe.config.tsworks with Vue, React, or vanilla HTML stories
Prerequisites
Before you begin, ensure you have:
- A Styleframe project with design tokens defined (Installation Guide)
- Node.js 18+
- Storybook 10+
Installation
Install Storybook and the framework adapter for your project:
pnpm add -D storybook @storybook/react-vite
yarn add -D storybook @storybook/react-vite
npm install -D storybook @storybook/react-vite
bun add -D storybook @storybook/react-vite
Setup
Create a Styleframe config
If you don't have one yet, create a styleframe.config.ts at your project root. This is the single source of truth for your design system.
import {
useDesignTokensPreset,
useGlobalPreset,
useModifiersPreset,
useSanitizePreset,
useUtilitiesPreset,
} from '@styleframe/theme';
import { styleframe } from 'styleframe';
const s = styleframe();
useDesignTokensPreset(s);
useSanitizePreset(s);
useGlobalPreset(s);
useUtilitiesPreset(s);
useModifiersPreset(s);
export default s;
Configure Vite with the Styleframe plugin
Add the Styleframe Vite plugin to your vite.config.ts. The scanner.content option tells the plugin which files to scan for utility class usage.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import styleframe from 'styleframe/plugin/vite';
export default defineConfig({
plugins: [
styleframe({
scanner: {
content: [
'./stories/**/*.stories.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
},
}),
react(),
],
});
Configure Storybook
Create or update .storybook/main.ts to point at your story files and set the framework adapter:
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: [
'../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
addons: [
'@storybook/addon-docs',
],
framework: '@storybook/react-vite',
};
export default config;
Import styles in the preview
Create .storybook/preview.ts and import the compiled CSS. This single import makes all your design tokens, utility classes, and recipe styles available to every story.
import type { Preview } from '@storybook/react-vite';
import 'virtual:styleframe.css';
const preview: Preview = {
parameters: {
layout: 'centered',
},
};
export default preview;
storybook dev to start the dev server. The Styleframe plugin compiles your config and extension files on the fly, with hot module replacement for instant feedback.Writing Stories
Stories follow a three-file pattern: a *.styleframe.ts file to register the recipe, a component file to consume it, and a *.stories.ts file for Storybook.
Register a recipe
Create a *.styleframe.ts file alongside your story. The Vite plugin discovers it automatically and makes the recipe available via virtual:styleframe.
import { useButtonRecipe } from '@styleframe/theme';
import { styleframe } from 'virtual:styleframe';
const s = styleframe();
const button = useButtonRecipe(s);
export default s;
Build the component
Import the button runtime function from virtual:styleframe and pass variant props to compute class names. The function is fully typed — invalid colors, variants, or sizes are caught at compile time.
import { button } from 'virtual:styleframe';
interface ButtonProps {
color?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'light' | 'dark' | 'neutral';
variant?: 'solid' | 'outline' | 'soft' | 'subtle' | 'ghost' | 'link';
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
label?: string;
disabled?: boolean;
}
export function Button({
color = 'neutral',
variant = 'solid',
size = 'md',
label = 'Button',
disabled = false,
}: ButtonProps) {
return (
<button className={button({ color, variant, size })} disabled={disabled}>
{label}
</button>
);
}
Write the story
Create a *.stories.ts file using Storybook's Component Story Format (CSF):
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from '../src/components/Button';
const meta = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
color: {
control: 'select',
options: ['primary', 'secondary', 'success', 'info', 'warning', 'error'],
},
variant: {
control: 'select',
options: ['solid', 'outline', 'soft', 'subtle', 'ghost', 'link'],
},
size: {
control: 'select',
options: ['xs', 'sm', 'md', 'lg', 'xl'],
},
label: { control: 'text' },
disabled: { control: 'boolean' },
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
label: 'Button',
},
};
export const Primary: Story = {
args: {
color: 'primary',
label: 'Primary',
},
};
export const Outline: Story = {
args: {
variant: 'outline',
label: 'Outline',
},
};
tags: ['autodocs'] option tells Storybook to auto-generate a documentation page for the component from its stories and prop types.Dark Mode
Styleframe themes are driven by the data-theme attribute on the DOM. To toggle dark mode in Storybook, listen for a theme change event in your preview and update the attribute:
import 'virtual:styleframe.css';
const channel = (await import('storybook/preview-api')).addons.getChannel();
const { DARK_MODE_EVENT_NAME } = await import('@vueless/storybook-dark-mode');
channel.on(DARK_MODE_EVENT_NAME, (isDark: boolean) => {
if (isDark) {
document.body.dataset.theme = 'dark';
} else {
delete document.body.dataset.theme;
}
});
This requires the @vueless/storybook-dark-mode addon. Install it and add it to your .storybook/main.ts addons array:
pnpm add -D @vueless/storybook-dark-mode
yarn add -D @vueless/storybook-dark-mode
npm install -D @vueless/storybook-dark-mode
bun add -D @vueless/storybook-dark-mode
const config = {
// ...
addons: [
'@storybook/addon-docs',
'@vueless/storybook-dark-mode',
],
};
Once configured, a sun/moon toggle appears in the Storybook toolbar. Every recipe and utility that references theme-aware tokens (like @color.background or @color.text) updates automatically.
data-theme attribute on the body.Best Practices
- Keep one
styleframe.config.ts: Share the same config between your app and Storybook so stories reflect the real design system - Co-locate
*.styleframe.tsfiles: Place recipe registrations next to the stories or components that use them for easy discovery - Use
tags: ['autodocs']: Let Storybook auto-generate docs pages from your component props and stories - Scan story files: Include
./stories/**/*.stories.{ts,tsx}in the scanner content so utility classes used in stories are extracted - Showcase all variants: Create stories for each meaningful combination of recipe props — colors, variants, sizes, and states
- Test dark mode: Use the dark mode toggle to verify every component looks correct in both themes
FAQ
apps/storybook/), you can either symlink the config or reference the shared @styleframe/theme package that both consume. The point is that stories should reflect the same tokens and recipes your production app uses.scanner.content glob, utility classes like _display:flex, _gap:md, and _padding:lg work in templates and JSX just like they do in your app code.Create a new *.styleframe.ts file in your stories or components directory. The Vite plugin discovers it automatically:
import { useCardRecipe, useCardHeaderRecipe, useCardBodyRecipe, useCardFooterRecipe } from '@styleframe/theme';
import { styleframe } from 'virtual:styleframe';
const s = styleframe();
const card = useCardRecipe(s);
const cardHeader = useCardHeaderRecipe(s);
const cardBody = useCardBodyRecipe(s);
const cardFooter = useCardFooterRecipe(s);
export default s;
The card, cardHeader, cardBody, and cardFooter functions are then available via import { card } from 'virtual:styleframe'.
styleframe/plugin/webpack), but Vite is the recommended and tested path.*.styleframe.ts file or styleframe.config.ts, the Vite plugin triggers a hot update. Your stories reflect the new tokens, utilities, and recipe styles without a full page reload.@storybook/addon-a11y addon works out of the box for accessibility checks, and @storybook/addon-themes or @vueless/storybook-dark-mode can drive the data-theme attribute that Styleframe themes respond to.