Building a component library - Maccessible part 1: Setup

So I've decided to build a component library in the style of the classic Mac OS. But before we can get into crafting the juicy details of the components themselves we have some preparation and setup to do.

Setting up the repository structure

A common theme for this project is going to be to try to keep things as simple as possible. I know, another difficult challenge, right! It will be important for many reasons in the long run though.

Setting up the project with pnpm

I want to use storybook to render all the components while working on them, and also to showcase them once they're done. Before we can install this tool however, we must initialize the repo with a package file, in which we can list all of our dependencies and scripts. I've decided to use pnpm as a package manager, mostly because I've never used it before and I want to check it out. In practice it does exactly the same thing as npm does, but behind the scenes it stores the dependencies differently. Instead of putting every dependency in every project in an individual local node_modules folder, it puts all packages from all projects in one single place on your harddrive. It then links these packages into the local projects. This means that you won't have multiple copies of code taking up megabytes on your disk, each package will only exist once. And if you need multiple versions of the same package, pnpm will cleverly only download the changed files, saving even more space.

Install pnpm by following the instructions in the docs, and then let's initialize a project by running the following command in our terminal:


pnpm init

Now that we have a package.json file in our repo, the next step is to install Storybook.

Installing storybook for web components

Now we can move on to installing Storybook. We can do this by using a script that Storybook provides that installs dependencies and sets up a basic example folder with some stories and basic config. I think this is great, so let's use that. Just like npm allows you to download and instantly run scripts without having to install anything via the npx command, we can do exactly the same with the corresponding pnpx command:


pnpx sb init

This will give us a choice of what framework the Storybook installation should adapt to. If we were to choose React for instance, the example stories would contain React components, and some React addons would also be installed.

BUT! Here is an unclear part of the process!

Storybook claims to support web components (and they do) but it seems like what they mean by that is primarily supporting web component frameworks like Google's Polymer, or web comopnents written using lit-html. I might use these in the future, as they might make the process of building web components easier, but I don't have the need for them at the moment.

And here a weakness in Storybook reveals itself.

Jan Miksovsky wrote an article on the same subject, and I agree with his assesment that Storybook was written with React and similar frameworks in mind, and therefore web components are not a first class citizen in the Storybook world. It took a good long while and I had to sift through a lot of technical errors and experimentation before I could make it work the way I wanted.

The gist of the solution is this: web components are just html, css, and JavaScript. If we can register a component with the browser we can then render it with plain html. So therefore, when the install script asks you for which kind of project you want to install Storybook, do not choose web components, choose html.

This results in a cleaner installation without unneeded dependencies and the examples can be reduced to simple html strings. Only a few teaks are needed.

Configuring Storybook to work with plain web components

Here is the final configuration for our project at this stage. Inside the .storybook folder two configuration files live. Main.js contains configuration regarding where our story files live, and what plugins to load. Preview.js contains code that will be run inside the iframe where the component will be displayed. I chose this file to import the component itself, since the component code includes the registration code. As long as this has run once we can then render the component in the story.


module.exports = {
stories: ["../components/**/*.stories.js"],
addons: ["@storybook/addon-links", "@storybook/addon-essentials"],


import "../components/button/button";

You'll note that the search blob for the stories point to the components folder. That's because I want my story files to live right next to my component files in their respective folders, instead of in their own folder (which the Storybook installation gives us).

So, inside of components/button there are two files, the component file and the story file.


class MacButton extends HTMLElement {
connectedCallback() {
this.innerHTML = <button>MacButton</button>;

customElements.define("mac-button", MacButton);


export default {
title: "Example/Button",

export const Primary = () => <mac-button></mac-button>;

The package.json contains the following devDependencies:


"@babel/core": "^7.12.10",
"@storybook/addon-actions": "^6.1.15",
"@storybook/addon-essentials": "^6.1.15",
"@storybook/addon-links": "^6.1.15",
"@storybook/html": "^6.1.15",
"babel-loader": "^8.2.2"

So that's it really. Not such a complicated setup after all, but we'll se if this causes any problems in the future.