← All pages Practise what we preach. View this page in:
Vefskólinn · Component libraries

One piece at a time

How do you eat an elephant? One piece at a time. Component libraries are how web teams do that with big products. This page goes through the ideas behind them one by one, and shows each idea in two Icelandic design systems: Hanna from Reykjavík City and Ísland.is from Digital Iceland. They solve the same problems in opposite ways, which makes each idea easier to see.

Hanna · Reykjavík City The CSS is the product.

You write HTML with Hanna's class names and link one stylesheet. Any website can use it.

Storybook: hanna-docs.reykjavik.is ↗
Ísland.is UI · Digital Iceland The React component is the product.

You use <Button>, and the CSS is an internal detail. It's built for apps inside the Ísland.is codebase.

Storybook: ui.devland.is ↗
Part 1

The problems it solves

You probably know these from earlier group projects. Here is how a component library deals with each one.

Dividing the work

Split a big product into small parts with clear borders. Then several people can work at the same time without getting in each other's way.

Hanna
modules/hanna-css/src/css/
  -basics.css.ts
  Alert.css.ts
  ButtonPrimary.css.ts
  ButtonSecondary.css.ts
  … over 100 more

modules/hanna-react/src/
  ButtonPrimary.tsx
  ButtonSecondary.tsx
  …

Every component has its own CSS source file, and the React package has a matching component file. Changing the Alert means opening one file.

github.com/rvk-utd/hanna ↗
Ísland.is UI

Every component is a folder with four files:

  • Button/Button.tsxthe React component
  • Button/Button.css.tsthe styles
  • Button/Button.stories.tsxwhat Storybook shows
  • Button/Button.spec.tsxtests

One person can own the Button folder, write its story and its tests, and never touch anyone else's code.

github.com/island-is/island.is ↗

In your project: split the work by component, not by page. “You make Button and Card, I make Header” works. “You make the front page, I make the about page” ends with two different buttons.

Where to start, one piece at a time

Build from the bottom up. First the tokens (colours, spacing, type), then the smallest components, then bigger components made from the small ones. Storybook lets you work on one piece at a time, with the rest of the product out of the way.

Hanna
@import "/css/v0.8/-basics.css";       /* 1. tokens */
@import "/css/v0.8/ButtonBar.css";     /* 2. small pieces */
@import "/css/v0.8/Icon.css";
@import "/css/v0.8/ButtonPrimary.css"; /* 3. the button */

This is what styles.reykjavik.is sends back when you ask for one button. The tokens come first, then the smaller pieces the button is built from, then the button itself.

Ísland.is UI
// libs/island-ui/core/src/lib/Button/Button.tsx
import { Box } from '../Box/Box'
import * as styles from './Button.css'
import { Icon } from '../IconRC/Icon'

The Button is built from smaller components: Box for layout and Icon for icons. Its styles read from a separate theme library that holds the tokens. Button.stories.tsx shows the Button on its own in Storybook, with no page around it.

In your project: start with the tokens and the smallest component. You can't build a Card before you have a colour palette and a Button.

Consistency: change it in one place

When every component takes its colours, spacing and fonts from the same set of tokens, the product looks consistent. One change to a token updates every component that uses it.

Hanna
/* -basics.css */
:root {
  --color-esja-100:     #a5bbfd;
  --color-faxafloi-100: #0367e1;
  --color-heidmork-100: #d8222f;
  --font-family: Esja, Helvetica, Arial, sans-serif;
}

/* ButtonPrimary.css */
--Button--color: var(--color-faxafloi-100);

CSS variables. The primary button, the secondary button, links and the main menu all read --color-faxafloi-100.

Ísland.is UI
// libs/island-ui/theme/src/lib/colors/colors.ts
export const blue400 = '#0061ff'
export const dark400 = '#00003c'
export const mint400 = '#00e4ca'

// Button.css.ts
backgroundColor: theme.color.blue400,

TypeScript constants in the theme library. Each component's .css.ts file imports them from there.

Try it: change the main colour

Both systems change in one place. The difference is when the change reaches the browser. On the left is Hanna's real CSS from styles.reykjavik.is. On the right is a simulation of the Ísland.is setup.

Hanna: a CSS variable
styles.reykjavik.is/bundle/v0.8?m=-basics,ButtonPrimary,ButtonSecondary
LiveThe browser updates immediately. No build step.
:root {
  --color-faxafloi-100: #0367e1;
}
Ísland.is setup: a TS constant
localhost:4173 · dist/assets/index.css
BuiltThe CSS matches theme.ts.
._7sc4nl1 { background-color: #0061ff }

Simulation: the hash here is computed in your browser, not by vanilla-extract. The buttons come from the replica in Exercise 2.

In your project: never type a colour value straight into a component. Give it a name once, and use the name everywhere.

Part 2

The programming ideas underneath

These are general programming ideas, not just CSS ideas. Design systems make them easy to see, because the result shows up on screen.

Abstraction

Hiding the details behind a simple surface, so you can use something without knowing how it works inside.

Hanna What you write (and all Storybook shows)
<button class="ButtonPrimary">
  Submit application
</button>
What's hidden underneath
<link rel="stylesheet" href=
"https://styles.reykjavik.is/bundle/v0.8?m=-basics,ButtonPrimary">

// modules/hanna-css/src/css/ButtonPrimary.css.ts
import { css } from 'es-in-css';

export default css`
  ${DEPS('Icon', 'ButtonBar')}
  .ButtonPrimary {
    ${ButtonStyle()};
  }
`;

The stylesheet link isn't in Storybook. Behind it is TypeScript that returns a string of CSS. A build step turns it into plain .css files and puts them on the city's style server.

Ísland.is UI What you write (and all Storybook shows)
<Button variant="primary">
  Submit application
</Button>
What's hidden underneath
import { Button } from '@island.is/island-ui/core'

// tsconfig.base.json (at the repo root)
"paths": {
  "@island.is/island-ui/core":
    ["libs/island-ui/core/src/index.ts"]
}

// libs/island-ui/core/src/lib/Button/Button.css.ts
const buttonBase = {
  fontWeight: theme.typography.semiBold,
  borderRadius: 8,
}

@island.is/island-ui/core looks like an npm package, but it's a path alias that points to a folder in the same repo. The styles are JavaScript objects that vanilla-extract turns into CSS when the app is built.

When you first open these Storybooks, the CSS and the import statements seem to be missing. That's abstraction doing its job. The cost shows up when something breaks, because then you have to dig down through the layers. The exercises below practise exactly that.

In your project: a good component hides its insides. Someone using your Card should only need to know its props, not how you built it.

Modularity

The system is made of separate modules with clear borders, and you take only the ones you need.

Hanna
<link rel="stylesheet" href=
"https://styles.reykjavik.is/bundle/v0.8?m=-basics,ButtonPrimary,Alert">

You name the modules you want in m=. Each name is one CSS file on the server. A page that only needs a button and an alert loads only those.

Ísland.is UI
import { Button, Box, Stack } from '@island.is/island-ui/core'

// libs/island-ui/core/src/index.ts
export * from './lib/Box/Box'
export * from './lib/Stack/Stack'
// … one line per component

One entry file re-exports every component. You pick modules with named imports, and the build tool follows those imports to decide what ends up in your app.

In your project: each component should work on its own. If you can't use the Card without also bringing in the whole front page, it isn't a module yet.

Dependencies

A dependency is something a module needs in order to work. You can't avoid them. What matters is whether they are written down and handled for you, or hidden until something breaks.

Hanna Inside the system: declared and automatic
// in the source
${DEPS('Icon', 'ButtonBar')}

/* in the compiled CSS on the server */
/*!@deps Icon, ButtonBar */.ButtonPrimary{…}

The button says what it needs, and the style server adds those files for you. You asked for ButtonPrimary and got Icon and ButtonBar too.

Outside the system: a version limit
$ npm install @reykjavik/hanna-react
npm error ERESOLVE unable to resolve dependency tree
npm error Found: react@19.3.1
npm error Could not resolve dependency:
npm error peer react@">=16.8 <19" from @reykjavik/hanna-react@0.10.174

The React package only accepts React 16.8 to 18. A new Vite project comes with React 19, so npm refuses to install it (tested 21 October 2026). You would have to move the project to React 18, or force the install with --legacy-peer-deps and hope nothing breaks.

Ísland.is UI Inside the repo: one hidden dependency
// libs/island-ui/core/src/index.ts (the first lines)
// Note: We use build-time styles and need to override
// global/box styles with component styles on the same DOM
// element. This means that the order of styles matters. […]
// in practice, the order of imports in this file can
// affect style ordering.

Inside the Ísland.is repo every app shares the same setup, so most dependencies are handled for you. But this comment describes one you can't see in the code: moving an import line up or down can change which styles win.

Outside the repo: too much to carry

In 2021 Digital Iceland published an alpha of the library on npm as @island.is/ui. Their own setup guide asked you to install all of this first:

yarn add @rehooks/component-size animejs classnames date-fns downshift hypher lodash markdown-to-jsx react react-animate-height react-datepicker react-dropzone react-keyed-flatten-children react-select react-toastify react-use reakit treat@1.6.0 -E

You also needed exact versions of Next.js 10 and webpack 4. With Create React App, you had to eject it and turn off TypeScript's strict mode. The library never left alpha. The last version is 0.0.0-alpha.5.

github.com/island-is/island-ui-next-example ↗

In your project: every dependency is a promise you have to keep. Before you make a component depend on something, ask whether it could live without it.

Decoupling

Keeping parts independent of each other, so you can change or replace one without breaking the others.

Hanna
<!-- plain HTML -->
<button class="ButtonPrimary">Submit application</button>

// React, with @reykjavik/hanna-react
<ButtonPrimary>Submit application</ButtonPrimary>

// … and inside hanna-react/src/ButtonPrimary.tsx
export const ButtonPrimary = (props) => (
  <Button bem="ButtonPrimary" {...props} />
);

The CSS doesn't know React exists. The React component's main job is to put the right class name on a button. The same CSS also serves plain HTML and Twig templates. If the city replaced React tomorrow, the styles would still work.

Ísland.is UI

The React components, the vanilla-extract styles, the theme and the repo's build setup all depend on each other. That gives the team type checking and one consistent setup. It's also why the library is so hard to use outside the repo, as you saw under Dependencies.

The design itself is decoupled from the code in one way: the Figma file is open to everyone, so designers can use the system even though outside developers can't install it.

In your project: tight coupling isn't always wrong. Ísland.is chose it on purpose. But know what each part depends on, so you know what will break when you change it.

Part 3

Why people argue about it

Component libraries are full of choices that good teams make differently. Hanna and Ísland.is land on opposite sides of the biggest one.

Runtime or build time

Should styles be plain CSS that the browser can still change while the page runs? Or should JavaScript generate them when the app is built? This is the long-running argument about CSS-in-JS. Tailwind, BEM and CSS Modules are other positions in the same debate.

Hanna · runtime CSS variables What the browser receives
.ButtonPrimary {
  --Button--color: var(--color-faxafloi-100);
  --Button--textColor: var(--color-suld-0);
  background-color: var(--Button--backgroundColor);
  border-radius: .1875rem;
  …
}
  • You can change a token in the browser. You did it in the colour demo above.
  • Class names are readable in DevTools.
  • Works with any framework, or none.
  • Class names are global, so two components can clash.
  • A typo in a class name fails silently. The button just isn't styled.
Ísland.is · build-time CSS-in-JS What the browser receives
/* npm run build */
._7sc4nl0 { border-radius: 8px; font-weight: 600; … }
._7sc4nl1 { color: #fff; background-color: #0061ff }

<!-- npm run dev -->
<button class="Button_variants_primary__7sc4nl1
               Button_base__7sc4nl0">
  • TypeScript catches a misspelled token. theme.color.blu400 is an error before you even run the code.
  • Hashed class names never clash.
  • Changing a colour means building the app again.
  • DevTools shows ._7sc4nl1, not a name you recognise.
  • Tied to one build setup.

This output comes from the replica in Exercise 2. Ísland.is produces the same kind of CSS.

In your project: neither side is simply right. Each team picked what fits its product. Hanna has to serve many city websites built with different tools. Ísland.is serves apps inside one repo, built by one organisation.

Specific to your product

Generic component libraries and AI tools give you generic components. A design system holds the things that are only true for your product.

Hanna

The colours are named after places and weather around Reykjavík: Esja (the mountain), Faxaflói (the bay), Heiðmörk (the forest), Elliðaárdalur (the valley), Bláfjöll (the ski area), Rökkur (twilight) and Súld (drizzle). The typeface is called Esja too.

Hanna also has named colour themes: trustworthy, dependable, friendly, lively and colorful.

[data-color-theme="friendly"] {
  --theme-color-primary:   var(--color-ellidaardalur-100);
  --theme-color-secondary: var(--color-rokkur-100);
}
Ísland.is UI

The Application System section of the Storybook has components that no generic library would include, built for Icelandic government forms:

  • NationalIdWithNameFormFielda kennitala field paired with the person's name
  • PaymentChargeOverviewwhat an application will cost
  • FileUploadFormFieldattachments for an application
An AI assistant can write a button in seconds. It can't know that your button should be Faxaflói blue, or that a government form needs a kennitala field. Knowing the product, and turning generic code into code for that product, stays your job. One popular library, shadcn/ui, takes this idea furthest: you don't install it as a dependency. You copy its components into your own project and change them to fit.

In your project: before you ask an AI for a component, write down your tokens and the rules your product follows. Then the generated code has something specific to follow.

Try it yourself

Exercises

Exercise 1 takes five minutes and only needs a browser. It covers modularity, dependencies and changing a token in one place. Exercise 2 rebuilds the Ísland.is setup in a small Vite project, so you can see the hidden layers from the inside. Exercise 3 is optional and runs the real Ísland.is Storybook.

Time: 5–15 minYou need: a browser and a text editorGoal: find the CSS that Storybook hides
  1. Create index.html

    A new folder with one file. Notice the <link> tag. That's all it takes.

    index.html
    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Hanna test</title>
      <link rel="stylesheet"
            href="https://styles.reykjavik.is/bundle/v0.8?m=-basics,ButtonPrimary,ButtonSecondary,Alert">
    </head>
    <body>
      <main style="padding: 2rem; display: grid; gap: 1.5rem; max-width: 40rem">
        <p>This page only uses HTML and classes. All the CSS comes from styles.reykjavik.is.</p>
    
        <div class="Alert Alert--success">
          <p>Your application has been submitted.</p>
        </div>
    
        <div style="display: flex; gap: 1rem; flex-wrap: wrap">
          <button class="ButtonPrimary">Submit application</button>
          <button class="ButtonSecondary">Cancel</button>
          <button class="ButtonPrimary ButtonPrimary--destructive">Delete</button>
        </div>
      </main>
    </body>
    </html>
  2. Open the file in a browser

    Double-click it or drag it into a browser window. No server, no npm install.

    You should see: a green notification box with a check mark, and three buttons: blue, white with a blue outline, and red.

  3. Find the CSS in the Network tab

    Open DevTools (F12), go to Network and reload. You'll see bundle/v0.8?m=… followed by one .css file per component. Click ButtonPrimary.css and look at the Response.

    Notice that the URL also loaded Icon.css and ButtonBar.css, which you didn't ask for. The system knows what each component depends on. That's the dependency handling from Dependencies at work.

  4. Trace the colour back to its token

    Right-click the blue button, choose Inspect and look at Styles. You'll find --Button--color: var(--color-faxafloi-100). Click the variable to jump to its definition in -basics.css. Change the value right there in DevTools.

  5. Override the token in your own CSS

    Add this after the <link> tag and reload:

    <style>
      :root {
        --color-faxafloi-100: #7c3aed;
        --color-faxafloi-150: #5b21b6;
      }
    </style>

    You should see: the primary button turns purple, and so does the outline button. One change, and every component that uses the token follows. -150 is the darker shade used on hover.

  6. Add a new component from Storybook

    Open Hanna's Storybook, pick a component and open the HTML tab in the panel at the bottom. Copy the HTML into your page.

    Then add the component's name to m= in the URL, for example …,Alert,TextInput. The names match the file names in hanna-css/src/css.

    If a component looks unstyled, its name is missing from m=. Interactive components like Carousel and Modal also need JavaScript to work, and that doesn't come with the CSS.