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>andargs - The trap: a
.sveltefile 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:
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 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:
Then start it:
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:
: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:
The catalogue never loads your layout, so it needs its own import, as the first line of the file:
The component¶
<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.childrenis 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:
<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
PrimaryButtonandDangerButton: 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.
argsare its props, and the text between the tags is itschildren. - The catalogue does not load your layout: import
tokens.cssin.storybook/preview.jstoo.
Quick check¶
1. What makes a component reusable?
- It lives in a file with the
.svelteextension - 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
variantas a prop to the component for that story - It changes the default value of
variantin 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.cssis imported in the layout but not in.storybook/preview.js - The story needs an
argsvalue 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-packagewith 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.