Before and after: Two loops for one component. Page-driven loop: Build the component inside a page, See only the happy path a page hits, Props harden around one call site, A new state later forces rework. Story-driven workshop: Design each state in isolation first, Every state is its own story, API settled before any page, Compose a component that survives.

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.

Story-driven versus page-driven component loopsThe page-driven loop discovers a component's real API late, at one call site, so a new state forces rework. The story-driven loop designs every state in isolation first, so the API is settled before the component enters a page.

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.

A workshop in miniature

Controls

Edit an arg. The preview re-renders the same way a Storybook control does.

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.

Edit the args on the left, then switch the view above the preview. The preview, the generated Button.stories.tsx source, and the announcement line all react to the same change. The default view renders without JavaScript.

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 · ts
// 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.

The Vite builder versus the legacy webpack builder, by architectural fact
ConcernVite builderLegacy webpack builder
Dependency handlingPre-bundled once with esbuildRe-bundled through the JS pipeline
Story source servingNative ES modules, per fileBundled module graph
Dev server startupNear-instant, regardless of app sizeGrows with the size of the app
Hot update on saveJust the changed moduleRebuild of the affected chunks
Status in StorybookThe default 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.

terminal · bash
# 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 · ts
// .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;

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.

When Pico CSS fits, and when to reach for a design system or utility framework
Your situationPico CSSDesign system or utility framework
Content-forward pages: docs, blog, marketingGood fitOverkill
A prototype or a component workshopGood fitToo much setup
Markup-first components in isolationGood fitNot needed yet
A heavy, custom brand systemFights youReach for this
Dense application UI: grids, data tablesRuns outReach for this
A large team needing shared utilitiesToo thinReach 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.

Talk to us about a component library or frontend build

Keep reading