We recently launched a new version of bs.ch, the official website of the Swiss canton of Basel-Stadt. This project was a collaborative effort, with Liip delivering not only a content strategy but also a full-fledged frontend and a design system framework that we're excited to share.

But what exactly is a design system? In short, the so-called "digital design system", or "DDS," is a flexible framework that defines how every web application related to the canton of Basel-Stadt should look and behave. It's designed to handle the complexity of evolving designs, empowering developers to recreate the look and feel of our website without being tied to a specific framework.

So, how can other agencies or even the canton itself recreate the design we developed for bs.ch visually and in code? The answer lies in a straightforward and user-friendly framework or library. It's not tied to a specific frontend framework, such as Vue or React, and it's designed for easy installation and use, making replicating the bs.ch design a seamless experience.

But first things first.

Establishing the foundation: Our use of Tailwind

At the very start of the project, we decided to use Tailwind and PostCSS as our CSS framework of choice. It offers flexibility through its atomic and utility-first approach, is not opinionated, is fully configurable, fast, modern, and extensible, and allows custom CSS to live next to and use it. So, component classes like .button would still be possible and could be created with Tailwind utility classes via the @apply directive. These benefits make it also perfect for an installable DDS in the long run.

A disclaimer here: We know that opinions about Tailwind differ widely. This post will not discuss the pros and cons of Tailwind but how we used it.

The first step for us was to configure it. We worked closely with our UX team for that and decided on colour palettes, typography settings, spacing, and much more before implementing it. Once that was done, it was implemented in Tailwind's config and Figma. Since we used the same language, namely Tailwind's naming conventions, Figma would reflect known properties, such as font sizes, colours and spacings, by their Tailwind names, such as text-xl, bg-green-100 or p-12, making it much easier for us developers to choose the right classes when implementing the designs in our frontend application.

Once the designs took form, we started implementing them. Whenever we encountered components that would be heavily reused, such as buttons, inputs or specific sections, we would extract them as Tailwind components in their own CSS file, knowing that, in the long run, attaching a button or input class would make it simpler for other developers using the DDS as well. And so, our Tailwind setup slowly grew over time, maturing in a fully-fledged Tailwind and PostCSS config, a collection of components and HTML-based examples for structures, sections, pages and more.

The design: Figma as the single source of truth

With the website's implementation, the Figma setup also became more sophisticated. More components were added, more pages were refined, and fixes were implemented based on feedback.

Since Figma is component-based, it could be used to create various designs, also for other pages and complex forms that didn't exist yet. Of course, since the canton wanted a consistent CI/CD, Figma could be used as a single source of truth for all agencies and teams working on pages for the canton. After all, all the details have already been implemented, and re-implementing them would require much effort.

And so we did. Once onboarded, the canton's employees could build designs based on bs.ch in Figma. Other teams could review these designs and implement them in their web applications.

From a design perspective, Figma became the canton's single source of truth for its CI/CD.

The decoupling: How we made our Tailwind setup installable

Once Figma and the Tailwind setup were sophisticated and "done" enough, we could start decoupling the CSS from our setup. As already mentioned, we needed to be agnostic to the frontend framework and, ideally, the build tool, but we could still use PostCSS and Tailwind since they also do not rely on a specific build tool.

So, we would keep the Tailwind and PostCSS setup and open-source that, right? Well, kind of. We wanted to make it installable. We essentially faced two problems:

First, we needed to ensure the PostCSS and Tailwind config would work. When you set up PostCSS in any build tool, PostCSS looks for its config in a predefined way. In some cases, such as Vite, you could overwrite where PostCSS tries to find the config; other build tools, such as Webpack, however, only allow that with significant effort.

Second, these two config files were only some things that had to be published. We also needed to publish a collection of icon SVGs, fonts, images, videos and CSS files. There are a myriad of options for custom icons. We would need a unified solution if we wanted a consistent look and feel styled with Tailwind. The same goes for the CSS files: Although based on Tailwind and PostCSS, they would need to be included in the build process in a unified way while still allowing for custom CSS to be added.

Luckily, adjacent to the Vite/Nuxt/Vue ecosystem, another ecosystem called UnJS exists. UnJS has many libraries and tools written in an 'agnostic way,' one of which is unplugin-icons. This library can be installed via NPM and offers a single plugin that works in most popular build tools. It converts icons to components of the frontend framework used, such as Vue, web components, React, Svelte, etc., and offers a unified syntax for using these icons across all frameworks.

This unplugin would solve our icon issue: We could deliver the icons as SVGs, use unplugin-icons to install them as components, and we'd be good.

However, it also sparked an idea: What if the entire DDS was an unplugin? It could install PostCSS and Tailwind with our config, include all CSS files and other assets, and use unplugin-icons to supply the user with icons in a unified manner. We could offer a few config options, pass the config for unplugin-icons and make the usage a matter of a few lines of code.

And so we did.

For example, consider using Vite and wanting to use the DDS in your web application. You could include the plugin in vite.config.js like so:

import KantonBSDesignsystemPlugin from '@kanton-basel-stadt/designsystem/vite'

export default defineConfig({
  plugins: [
    KantonBSDesignsystemPlugin({ /* Options */ }),
  ],
  server: {
    fs: {
      strict: false,
    }
  },
})

In Webpack5, it would be very similar:

// webpack.config.js
module.exports = {
  /* ... */
  plugins: [
    require('@kanton-basel-stadt/designsystem/webpack').default({ /* Options */ })
  ],
}

To now use the Tailwind setup, add the following to your main CSS file:

@import '@kanton-basel-stadt/designsystem/assets/css/tailwind.css';

And that's it. Apart from some DOM to build the page, nothing else is necessary. Tailwind would take care of styling.

Behind the scenes, the DDS unplugin is a collection of three unplugins. The first one takes care of replacing all @kanton-basel-stadt/designsystem path references with absolute paths, the second one adds PostCSS and Tailwind to the build tool of choice, and the third one is unplugin-icons, delivering our custom SVGs. Since unplugins can be nested, everything can be installed with a single dependency, reducing the effort to get it up and running even further.

The documentation: Building serving suggestions with Storybook

The DDS's "third pillar" is the Storybook instance. Since we decided against building an entire component library for every framework, we needed a way to supply other teams with HTML. After all, having CSS alone, especially Tailwind, won't help recreate complex structures, such as the page header, various forms, or even emails.

So, we set up a Storybook instance to provide the components' HTML structure and further documentation. This way, developers could use the DOM in whichever framework they wanted. All they need to do is configure the component in Storybook—for example, change the button type, label, and icon—create a component in their framework of choice, and copy the DOM over.

Since Storybook also offers support for MDX (Markdown + JSX), we were able to add more documentation, such as tutorials on how to use the Tailwind setup, add iframes that linked to the Figma files, and even load the README file of the DDS unplugin and display it.

The Storybook instance, therefore, serves as an entry point for developers. It offers extensive documentation, all components necessary, links to further resources and takes the developers by the hand by providing lots of tutorials related to the DDS.

About accessibility in the DDS

One primary requirement for the entire design, especially the DDS, is accessibility. The canton's official website contains information essential to everyone, not just people with typical vision.

For this reason, we strived for WCAG2.1 AA and eCH-0059 Version 3.0 certifications. And to achieve these, we needed to follow its guidelines.

The library [Axe] (https://github.com/dequelabs/axe-core) is a simple way to test for most accessibility violations automatically. While we already used Axe as an overlay for developers on the front end, we also introduced it as a plugin for the Storybook instance. With Axe enabled, people using the Storybook instance can see that it conforms to accessibility standards.

Since we don't deliver full-fledged HTML with the installable Unplugin, we need to provide a working baseline that can be expanded upon. When developing new components, the core team develops them in an accessible way, with sensible defaults for other teams and developers to pick up and build upon.

Accessibility isn't just contrast and font weights, either. It includes other things, such as aria-* attributes, hidden labels for screen readers, semantic HTML, and much more, all of which are reflected within the Storybook components.

Why we open-sourced it

We decided to release the DDS under the GNU General Public License v3.0 for several reasons.

First, we wanted to create a public issue queue that everyone could follow without the need for extra accounts on any platform other than Github, which developers are very likely already using.

Second, the Swiss national government is already following a strategy to open-source crucial software for transparency. The canton itself follows the principle of "public money for public code", so open-sourcing the DDS follows this government strategy and principles.

Third, we wanted to allow collaboration and contributions by the people. Many people use the website bs.ch and its ecosystem, and many are experts. If an expert spots a problem or has an idea to improve the DDS, they can now contribute to the DDS by opening up pull requests or reporting issues on GitHub. For example, let's say that a component has a contrast issue. The text colour is too light for the text size to provide enough contrast to be legible for people with visual impairments. Opening up an issue on the Storybook repository and linking to the component with the problem with an explanation or even ideas on how to fix it helps significantly to improve accessibility.

Conclusion

The three pillars of the digital design system, Figma, the unplugin and the Storybook instance, provide a unified, flexible, maintainable and modern way to implement the CI/CD of the canton of Basel-Stadt.

While Figma lets people create visual designs based on existing components, the unplugin provides all the tools necessary to implement them, and the Storybook instance offers all the essential scaffolding and structure.

By open-sourcing the DDS on GitHub, we allow for collaboration, follow a modern approach to public sector software, and provide true transparency.

If you want to see it in action, head to the Storybook instance: kanton-basel-stadt.github.io/storybook/