Skip to content

Storybook and your first component

Exercise storybook-first-component-annex.pdf · PDF

Open your NoteBox code and search for <button. How many did you find? Now the designer says: "corners go from 8 px to 12 px." How many files do you open?

If you copy and paste, you have two problems. If you paste eight times, you have eight. In Figma you already solved this: one main component, many instances. Change the main, and every instance follows. This unit does the same thing in code, and this page builds the first piece.

What you will learn

  • Explain what a reusable component is, and why copy-paste does not scale
  • Name the five tools of this unit and the job of each
  • Install Storybook in a SvelteKit project and find your way around it
  • Read a Figma component spec and turn it into a Svelte component with design tokens
  • Write stories with defineMeta, <Story> and args
  • The trap: a .svelte file is not automatically a reusable component

The brick

A reusable component is a piece of interface built once, with a fixed connection, and used in many places. It knows nothing about the page it lives in: it receives everything through its connection.

LEGO filed the patent for its stud-and-tube brick on 28 January 1958, and a brick from that year still fits one made today. Why? The connection never changed. Nobody asks how a brick is made inside. You only need to know its studs.

We keep LEGO for the whole unit. The brick is the component. The studs are its props, the values you pass in. The model you build is the app.

LEGO In this unit Page
The brick A Svelte component in src/lib/components/ This page
The studs Its props, defaults, events and snippets Props and defaults, Methods, snippets and events
The catalogue Storybook: every brick on its own, in every colour This page and Visual tests and docs
Quality control Unit tests and visual tests Unit tests, Visual tests and docs
The boxed set An npm package Packaging
The model Your app, built from your own bricks Workshop and revision

Careful: not every .svelte file is reusable

A +page.svelte is a component for Svelte, but nobody reuses it. It knows its route and its data. Reusable means it knows nothing about where it is used.

Five tools, five jobs

Making a brick properly takes more than one tool. These five come back all through the unit:

Tool Its job Since
Figma Design the component: variants, properties, tokens. The blueprint. You know it from Unit 1
Storybook Build and show each component alone, in every state. Later, its documentation. 2016, as React Storybook; version 10 in October 2025
Vitest + Testing Library Unit tests: does the component behave as promised? Vitest: December 2021
Playwright Visual tests in a real browser: does it still look as promised? Microsoft, 31 January 2020
svelte-package + npm Pack the components so that another project can install them. npm: 2010

Figma designs. Vitest and Playwright test. Storybook sits in the middle: you design in code there, and you test by looking.

Read the blueprint

Before you write a line, read the spec. Click an instance of a Figma component and look at the right-hand panel. For our Button it lists what can change:

Figma property Values Svelte prop
Variant Primary, Secondary, Danger variant
Size Medium, Small size
Disabled On, off disabled
Label Any text children

The spec also names its colours. Open Local variables in Figma and you see nb/ink, nb/paper, nb/danger. In CSS we do not write #111111. We write var(--nb-ink). That is a design token (you met them in Design systems): the same name in Figma and in code, so that designer and developer speak one language. If your brand colour changes, you open one file.

Storybook: the catalogue

Storybook is a workshop for components. It runs as a separate website where you build and look at each component alone, with no page, no data and no login around it.

Why not test the button in your app? Because to see the disabled state you would have to log in, open a note and reach the right screen. To see the danger state, you would have to find a different screen. In a catalogue, every state is one click away.

Install it

In your project folder:

npm create storybook@latest

If the installer offers test features, leave them out: you will set up tests by hand in Unit tests. It detects SvelteKit and installs storybook, @storybook/sveltekit, @storybook/addon-svelte-csf and @storybook/addon-docs. It may instead stop with this error:

npm error ERESOLVE unable to resolve dependency tree

npm picked two different Storybook versions. Look up the version of storybook in your package.json, say 10.6.4, and ask for the same one:

npm install -D @storybook/svelte@10.6.4

Then start it:

npm run storybook

It opens http://localhost:6006. Your app still lives at localhost:5173. Those are two different servers, and users only ever see the second one.

What the installer changes What appears
package.json The scripts storybook and build-storybook, and the new dev dependencies
.storybook/main.js Where the stories are, which addons, which framework
.storybook/preview.js What every story shares
src/stories/ Example components. Delete the folder: they are somebody else's design
.gitignore Two new lines: *storybook.log and storybook-static

If port 6006 is busy, another Storybook is still running in a forgotten terminal. If nothing works at all, check that you are in the right folder: there must be a package.json next to you (ls).

Find your way around

Area What it is
Sidebar The catalogue index. One entry per component, one sub-entry per story
Canvas The component, alone, with nothing around it
Controls One input per prop. Change it and the canvas changes. This is Figma's right-hand panel, in code
Actions A log of the events the component fires. You will use it in Methods, snippets and events

A story is one component in one specific state. Button · Primary. Button · Disabled. Think of a catalogue photo: this brick, in red.

Careful: Storybook does not test by itself

It is a workshop, a place to build and look. It checks nothing for you. Tests are code that checks without you looking, and they come later in this unit.

Your first brick

Tokens first

Create the tokens file, using the names from the Figma table:

src/lib/tokens.css
:root {
    --nb-ink: #111111;
    --nb-paper: #ffffff;
    --nb-muted: #666666;
    --nb-line: #d4d4d4;
    --nb-accent: #2563eb;
    --nb-danger: #dc2626;
    --nb-warning-bg: #fef3c7;
    --nb-warning-ink: #78350f;
    --nb-success-bg: #dcfce7;
    --nb-success-ink: #14532d;
    --nb-radius: 8px;
    --nb-font: Inter, system-ui, sans-serif;
}

Import it in two places, because the app and the catalogue are two different websites. Your app loads it from the layout:

src/routes/+layout.svelte (add to the script)
import '#lib/tokens.css';

The catalogue never loads your layout, so it needs its own import, as the first line of the file:

.storybook/preview.js
import '../src/lib/tokens.css';

The component

src/lib/components/Button.svelte
<script>
    let { variant = 'primary', children } = $props();
</script>

<button type="button" class="btn {variant}">
    {@render children()}
</button>

<style>
    .btn {
        font: 500 16px/1 var(--nb-font);
        height: 48px;
        padding: 0 20px;
        border: 2px solid var(--nb-ink);
        border-radius: var(--nb-radius);
        cursor: pointer;
    }
    .primary {
        background: var(--nb-ink);
        color: var(--nb-paper);
    }
    .secondary {
        background: var(--nb-paper);
        color: var(--nb-ink);
    }
    .danger {
        background: var(--nb-danger);
        border-color: var(--nb-danger);
        color: var(--nb-paper);
    }
    .btn:focus-visible {
        outline: 2px solid var(--nb-accent);
        outline-offset: 2px;
    }
</style>

Three things to notice:

  • variant = 'primary': the default variant in Figma becomes the default value in code. The next page is all about this line.
  • children is the label: whatever you write between <Button> and </Button>. You met it in +layout.svelte.
  • The class is the variant's name. There is no if: class="btn {variant}" picks the CSS rule.

You will see nothing yet, because nothing uses this component. A brick in a drawer is not a model. That is exactly why we need the catalogue.

The first stories

The stories file sits next to the component:

src/lib/components/Button.stories.svelte
<script module>
    import { defineMeta } from '@storybook/addon-svelte-csf';
    import Button from './Button.svelte';

    const { Story } = defineMeta({
        title: 'NoteBox/Button',
        component: Button
    });
</script>

<Story name="Primary">Save</Story>

<Story name="Secondary" args={{ variant: 'secondary' }}>New note</Story>

<Story name="Danger" args={{ variant: 'danger' }}>Delete</Story>

Read it piece by piece:

Piece Meaning
Button.stories.svelte Storybook finds every *.stories.svelte under src/
<script module> Runs once for the file, not once per story. defineMeta must go here
defineMeta Says which component, and where it goes in the sidebar (NoteBox/Button). It gives back a Story component
<Story> One catalogue photo
args The props for that photo
Text between the tags The children

Save. Three stories appear in the sidebar, with no restart. Open Controls and type danger in variant. Press Tab: there is the blue focus ring from the spec.

Finally, use the brick for real. Replace one hand-written <button> in one of your pages with <Button>, and check that the page works as before.

When it goes wrong

What you see Why
The story shows an unstyled button tokens.css is not imported in .storybook/preview.js
The button is empty You forgot {@render children()}
The button shows the text of a function You wrote {children} instead of {@render children()}
Hex codes inside your component They belong in tokens.css. If your brand colour changes, you should open one file

Key takeaways

  • A reusable component knows nothing about the page it lives in. It receives everything through its props.
  • One component with a prop beats PrimaryButton and DangerButton: Figma taught you this with variants.
  • Design tokens keep the same names in Figma and in code, so a redesign is one file instead of forty.
  • Storybook is a separate website on port 6006, for developers. Users never see it.
  • A story is one component in one state. args are its props, and the text between the tags is its children.
  • The catalogue does not load your layout: import tokens.css in .storybook/preview.js too.

Quick check

1. What makes a component reusable?
  • It lives in a file with the .svelte extension
  • It knows nothing about the page and receives everything through props
  • It is imported by the layout of the whole application
  • It contains its own data and fetches what it needs

Every .svelte file is a component, but a +page.svelte knows its route and its data. A reusable component has a fixed connection, its props, and can be dropped into any page. That is the LEGO brick: nobody asks how it is made inside.

2. Your app runs on port 5173. Why does Storybook open on a different port?
  • Port 5173 is reserved for the finished production build
  • Storybook replaces your app while you are developing
  • Storybook is a separate server for developers, apart from your app
  • Each component needs its own server and its own port

Storybook is a second website, usually on port 6006, where components are shown alone. Your app keeps running on its own server, and users never see the catalogue.

3. What is a story?
  • A complete page of the app with fake data in it
  • A test that fails when the component changes
  • A paragraph that documents why a component exists
  • One component shown in one specific state

Think of a catalogue photo: this brick, in red. A story is not a page and it checks nothing by itself. Button · Disabled is one story, and Button · Primary is another.

4. In a stories file, what does args={{ variant: 'danger' }} do?
  • It passes variant as a prop to the component for that story
  • It changes the default value of variant in the component
  • It adds a new variant called danger to the Figma file
  • It tells Storybook to test the danger variant automatically

args are the props for that one catalogue photo. The component's own default stays as it was, and every other story keeps its own args.

5. Your button looks right in the app, but it has no colours in Storybook. What is the likely cause?
  • Storybook cannot show CSS custom properties
  • The <style> block of the component is ignored by Storybook
  • tokens.css is imported in the layout but not in .storybook/preview.js
  • The story needs an args value for each colour

The catalogue never loads your +layout.svelte, so a variable like var(--nb-ink) has no value there. Import the tokens in .storybook/preview.js as well, and every story gets them.

6. Which tool would you pick to check that a component still looks right after a change?
  • Vitest with Testing Library
  • svelte-package with npm
  • Figma with its variants
  • Playwright in a real browser

Playwright drives a real browser, so it can see what is drawn. Vitest and Testing Library check behaviour, Figma designs, and svelte-package packs the library.

7. A note card is copied into four pages, each with slightly different padding. The designer wants one look. What do you do?
  • Correct the padding by hand in each of the four pages
  • Move the padding values into a global CSS file for pages
  • Build one card component, then use it in all four pages
  • Create four components, one for each page that needs a card

One brick, used four times. With a single component, the next change is made once and every page follows, exactly like a main component and its instances in Figma.

Your button has one stud so far. In Props and default values you give it the rest, and decide what happens when nobody passes a value.