Component-driven development with Storybook and Vite, wired for a real-time loop
One deliberately chosen isolation stack. Storybook is the workshop, the Vite builder makes the loop real-time, and Pico CSS keeps your component markup honest.
This is written for the frontend software engineer who owns a component library and wants a faster feedback loop. Most tutorials sell component-driven development with Storybook and Vite as a benefits list. However, they never argue the one claim that matters. The claim is behavioral. When you build a component in isolation, you design its states and its interface before any page can paper over them. So the argument comes first, and the tooling second.
Two loops for one component#
The usual pitch says isolation is faster. That is true, yet it undersells the point. Isolation changes how you build, not just where. Because the workshop shows a component alone, you are forced to answer questions a page lets you dodge. What does this look like with no data? What happens while it loads? How does it fail? Each answer shapes a prop or a slot, and it shapes it early. The diagram below draws the two paths this splits into.
Every state, including the ones a page hides#
A page is a happy path most of the time. Therefore it rarely renders the awkward states. With real data present, you never see the empty message. Because the request already resolved, you never see the spinner. In isolation you flip that default. Empty, loading, error, and overflow become first-class stories, not accidents you stumble into weeks later.
This is where the design work actually happens. For example, once you have to draw the empty state, you discover the component needs an emptyLabel prop. Once you draw overflow, you learn the list needs a scroll region and a max height. In practice, those decisions are cheap in the workshop and expensive in a page. Building against every state in isolation is what surfaces them.
You shape the props and API first#
Read the diagram as two loops you can run. In the page-driven loop, you build the component inside a page and only ever see the states that page hits. Consequently the props harden around one call site, and a new state later forces rework. In the story-driven loop, you design each state in isolation first. Then you compose a finished component, whose API already survived every state, into the page.
That contrast is the heart of story-driven development versus page-driven development. If you want the API-contract side of this, our guide to building scalable, reusable UI components covers how to shape a prop interface that holds up. This page owns the isolation tooling that gets you there.
Feel the story-driven loop in a workshop#
The argument is easier to trust once you drive it. Below is a miniature component workshop. On the left sit the args, the same knobs Storybook's Controls addon would generate. Change the variant, edit the label, or flip disabled. The preview re-renders at once. Above the preview, a segmented toggle cycles the states a page rarely surfaces on purpose. Switch between default, empty, loading, error, and overflow. Watch the generated story object update in step.
Notifications
3 unread- Deploy #4821 finished in 2m 14s
- PR #317 approved by a reviewer
- Storybook build passed on the Vite builder
The happy path a page usually shows: a short list of items.
Button.stories.tsx
export const Panel = {
args: {
variant: 'primary',
disabled: false,
label: 'Mark all read',
},
// active view: default
};Preview showing the default state with a primary button labelled Mark all read.
Notice what the view toggle did. In a running app you would see the empty and error states only when real data happened to be empty or a request happened to fail. In isolation, each state is one click away. That is the loop. The rest of this guide is why three specific tools make it real.
The stack: three tools wired for one loop#
One loop needs three things: a place to render each state, a dev server fast enough that the loop feels instant, and a styling discipline that keeps the markup honest. Storybook, the Vite builder, and Pico CSS each own one of those jobs. Here is why each earns its place, tool by tool.
Storybook: the workshop#
Start with the tool that owns the workshop. Storybook renders your components outside the app, one state at a time. Moreover it gives each state a name, a URL, and a live control surface. That is what turns a folder of components into a place you can actually work.
A story is a rendered component state
The core unit is small. A story is one component rendered in one state, described by an object. You export a story per state, and Storybook lists them in the sidebar. Because each story is addressable, the empty state and the error state sit side by side, ready to inspect. That is the state gallery the workbench above imitated.
Args and controls turn props into live knobs
Args are the inputs a story passes to a component. When you declare argTypes, the Controls addon reads them and generates a UI automatically. The Storybook docs put it plainly. Controls "automatically generate UI controls based on your args", so a select, a checkbox, and a text field appear with no extra code.
// Button.stories.tsx — argTypes become the Controls UI, no extra code.
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'outline'] },
disabled: { control: 'boolean' },
label: { control: 'text' },
}, That is exactly what the left rail of the workbench did. You edited an arg, and the preview re-rendered. For the full control catalog, see the Storybook Controls documentation. In short, args make a component's props tangible while you design it.
It doubles as living documentation and a visual-test surface
A story earns its keep twice more. First, it is living documentation. Because a story renders the real component, the docs cannot drift from the code the way a static screenshot does. Second, it is a visual-test surface. Since each state is isolated and deterministic, a snapshot tool can diff it in CI. Therefore the same stories that helped you design the component also guard it against regressions.
The Vite builder: what makes it real-time#
Isolation is only useful if the loop is fast. This is the part most guides wave away with the word "faster". The honest answer is architectural. The Vite builder pre-bundles your dependencies once with esbuild, then serves your story source over native ES modules. As a result the dev server starts almost instantly, and a save updates one module rather than re-bundling a graph.
| Concern | Vite builder | Legacy webpack builder |
|---|---|---|
| Dependency handling | Vite builderPre-bundled once with esbuild | Legacy webpack builderRe-bundled through the JS pipeline |
| Story source serving | Vite builderNative ES modules, per file | Legacy webpack builderBundled module graph |
| Dev server startup | Vite builderNear-instant, regardless of app size | Legacy webpack builderGrows with the size of the app |
| Hot update on save | Vite builderJust the changed module | Legacy webpack builderRebuild of the affected chunks |
| Status in Storybook | Vite builderThe default builder | Legacy webpack builderLegacy, opt-in only |
The startup claim is not marketing. Vite's own documentation reports that "dev server start dramatically improved", because it stops bundling the whole app before serving. The hot-reload claim is the same shape. On a change, Vite updates just that module in the browser, without a full reload. You can read the reasoning in Vite's "Why Vite" documentation, and the builder specifics in the Storybook Vite builder documentation. Because the loop stays real-time as the library grows, isolation stays worth doing.
Pico CSS: the classless wedge#
Here is the piece no competitor covers. Styling in the workshop is not an afterthought, it is a discipline choice. Pico CSS is a classless, semantic-first stylesheet, and that is exactly what a workshop wants. The classless build is small too. Measured from the published minified file, pico.classless.min.css is 71,040 bytes raw and 10,432 bytes gzip, so roughly 10.4 kB over the wire.
Classless means you style semantic HTML directly
Most CSS frameworks want class names on every element. Pico's classless build does the opposite. It styles bare semantic HTML, so a plain <button>, <article>, or <form> already looks right. You import one file and write honest markup. For example, a form is just labels and inputs, not a scaffold of wrapper divs carrying utility classes. The homepage of the project shows this at the Pico CSS site, and the details live in the Pico CSS documentation.
A classless stylesheet forces honest, markup-first components
This is the behavioral payoff. When there are no utility classes to reach for, your story's markup has to carry the meaning. Consequently you write a real button, a real list, and a real form, and the component's API stays about behavior rather than styling. In practice, that keeps the markup clean before the component is ever tangled into a page. If you do want to explore heavier layout work, our piece on structuring complex layouts with CSS Grid pairs well, because Pico handles the semantics and Grid handles the composition.
Wire all three into one setup#
Now put component-driven development with Storybook and Vite plus Pico into one reproducible walkthrough. This is the part the search results lack. You will scaffold Storybook on the Vite builder, wire Pico into the preview, and write a first story with live controls.
Scaffold Storybook on the Vite builder#
Start in a Vite app. Then run the initializer. It installs Storybook v10.5.2 and auto-selects the Vite builder, so there is nothing to configure by hand. Because Storybook 9 shipped a "48% leaner, flatter dependency structure", the install is less than half the footprint of the Storybook 8 era, and it lands faster.
# In a Vite app, scaffold Storybook. The Vite builder is auto-selected.
npx storybook@latest init
# Installs Storybook v10.5.2 with @storybook/react-vite as the builder,
# then starts the workshop on a native-ESM dev server.
npm run storybook The leaner dependency structure is documented in the Storybook 9 release post. Next, wire the styling and write the first story.
Wire Pico into the preview and write a first story with controls#
Three small files finish the setup. First, main.ts points at the Vite builder. Second, preview.ts imports the classless Pico build once, so every story inherits it. Third, Button.stories.tsx declares argTypes, so the Controls addon renders live knobs. Switch between the three below.
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|tsx)'],
addons: ['@storybook/addon-essentials'],
framework: {
name: '@storybook/react-vite', // the Vite builder, the default
options: {},
},
};
export default config; // .storybook/preview.ts
// Import Pico's CLASSLESS build once. Every story now renders semantic
// HTML styled by Pico, with zero utility classes anywhere.
import '@picocss/pico/css/pico.classless.min.css';
import type { Preview } from '@storybook/react';
const preview: Preview = {
parameters: {
controls: { expanded: true }, // show the args table as live knobs
},
};
export default preview; // src/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Workshop/Button',
component: Button,
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'outline'] },
disabled: { control: 'boolean' },
label: { control: 'text' },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
// One story per state. Controls turns each arg into a live knob.
export const Primary: Story = { args: { variant: 'primary', disabled: false, label: 'Save' } };
export const Disabled: Story = { args: { variant: 'primary', disabled: true, label: 'Save' } };
export const Empty: Story = { args: { variant: 'outline', disabled: false, label: '' } }; That is the whole stack. A roughly 10.4 kB classless stylesheet, plus a default-Vite Storybook that starts near-instantly and hot-reloads a single component's args as you design its states. For where this sits in a broader delivery flow, our guide to an SEO-friendly frontend build covers the shipping side, and the reactive-UI patterns in building reactive interfaces with streaming state patterns show the same state-first discipline in a different framework.
When NOT to reach for this stack#
This stack is honest work, so be honest about its edges. Storybook is not free. It is a real dependency with a maintenance cost, and a tiny component set does not justify it. Pico has a boundary too. It is a content-forward and prototyping tool, not a design-system replacement. The table below draws both lines.
| Your situation | Pico CSS | Design system or utility framework |
|---|---|---|
| Content-forward pages: docs, blog, marketing | Pico CSSGood fit | Design system or utility frameworkOverkill |
| A prototype or a component workshop | Pico CSSGood fit | Design system or utility frameworkToo much setup |
| Markup-first components in isolation | Pico CSSGood fit | Design system or utility frameworkNot needed yet |
| A heavy, custom brand system | Pico CSSFights you | Design system or utility frameworkReach for this |
| Dense application UI: grids, data tables | Pico CSSRuns out | Design system or utility frameworkReach for this |
| A large team needing shared utilities | Pico CSSToo thin | Design system or utility frameworkReach for this |
In the end, the value of component-driven development with Storybook and Vite is not the tools. It is the discipline the tools make easy. You design every state and settle the API in isolation, then compose a component that survives contact with a real page.