writingartikelen

lintje: the Rijkshuisstijl, for applicationslintje: de Rijkshuisstijl, voor applicaties

There’s no shortage of components for Dutch government sites. NL Design System and RVO’s own set give you buttons, form fields, alerts and headers, all in the house style. For a website, that covers most of what you need.

But I wanted to build applications, a dashboard among them — and for that, a lot is missing.

Made for content websites

Most component libraries — the government ones, and plenty of others — are built for simple content websites: a page of text, a form, a header and a footer. They’re good at that. An application needs more: a data table you can sort, filter and edit, a drawer, tabs, a stepper, a tree view, a split pane, a multiselect, a date range, toasts and notifications, a confirm dialog, a warning before your session runs out. None of it is exotic. It’s just usually not there.

A dashboard adds more on top: charts, KPI tiles, a filter bar, and a navigation that sits beside the page instead of across the top.

Why not just add a chart library?

That’s the usual fix: government components for the forms, a chart library for the rest. It works, but you can tell. The library brings its own fonts, its own colours, its own tooltip and its own idea of dark mode. Next to the house style it looks like it came from another site. And whether you can read the chart by keyboard or screen reader is up to the library, not to you.

Take the data colours. The Rijkshuisstijl has them, but a chart library doesn’t know that. Nor does it know they have to stay the same in light, in dark and in every organization’s theme, so a red bar means the same thing everywhere. That’s easier to get right once, in one place, than on every page.

Why a side navigation?

A government website puts its navigation across the top: a handful of sections, a submenu per group, and the page scrolls underneath. An application gets used differently. You jump between ten or fifteen screens all day, on a wide monitor, and you want to see where you are without opening anything. A menu beside the page does exactly that. When you need the room, it folds into a narrow rail of icons.

The house style doesn’t have one — a website rarely needs it.

A dark mode that’s actually finished

What I also kept missing was a dark mode that’s carried through everywhere. Not a dark background with a bright white date picker on top, or a chart that still draws its gridlines for a white page. In lintje every component, every chart and every theme has a dark version, and every test run draws all of them in both modes.

Dark isn’t just light flipped around, either. It’s its own colour range, the same for every theme, with its own contrast rules. In dark mode the theme colour gets three variants — for a fill, a line and text — because each has a different contrast requirement. And a QR code always stays light, because camera apps want dark modules on a light background.

Motion that feels right

I wanted the animation to feel finished too. A chart draws its data in as it comes into view — once; scroll back and it doesn’t replay. A KPI counts up to its value, and a new number slides into place. What arrives slows down, what leaves speeds up. The whole interface shares one base duration; data motion takes a little longer.

Two rules keep it in check. A value that animates sits in the accessibility tree at its final value, so a screen reader never reads out a number halfway. And with reduced motion switched on, nothing disappears except the motion itself. The focus ring never moves.

So I built the rest

lintje — named after the ribbon (het lint) in the Rijksoverheid logo — is the Rijkshuisstijl as standard custom elements, built with Lit 3. One script registers all the lintje-* tags; on your page you write a tag and set its attributes.

<link rel="stylesheet" href="dist-elements/tokens.css" />
<link rel="stylesheet" href="dist-elements/fonts.css" />
<script type="module" src="dist-elements/lintje.js"></script>

<lintje-tile heading="Aanvraag">
  <lintje-text-input label="Naam"></lintje-text-input>
  <lintje-button variant="primary">Versturen</lintje-button>
</lintje-tile>

The house style is written for communication

The Rijkshuisstijl covers the colours, the typeface, the logo and the header of a government site. It doesn’t say what a selected table row looks like, how a chart behaves in dark mode, or where the menu goes on a phone. An application needs all of that.

lintje fills it in, in the spirit of the house style. What already exists, it simply uses — RijksSans and the icons come straight from RVO’s assets. The rest it adds:

  • Charts. Ten kinds, one visual grammar, no chart library: bar, horizontal bar, grouped, stacked, line, dual-axis, pie, heatmap, progress towards a target, and a map — of the Netherlands on PDOK geometry, or of the world. The data colours are fixed by the Rijkshuisstijl.
  • Two navigation layouts. The familiar bar across the top, and a layout with the menu at the side: pinned, or as a narrow rail that expands over the content. Below 768 px both become a header with a full-screen menu.
  • The parts of a dashboard. A shell with its menu, a page header, a filter bar, KPI tiles, a data table with editable cells. Over a hundred elements in all, in thirteen categories, from the page frame to a chat with your data.
  • Six themes. A theme sets just two properties, --primary and --accent; the rest is derived from them with color-mix() and OKLCH, in light and in dark.

Designed in Claude Design

The whole design — the components, their states, the dark mode and the phone layouts — was made in Claude Design, and the code follows it. The style guide in the repo shows every element in all its states, with its properties and events underneath, so you see the design and the code in one place.

WCAG AA as the floor

When the house style and WCAG AA clash, AA wins — with the smallest change that does the job. A few examples:

  • Colour is never the only thing that carries meaning. Next to a colour there’s always a symbol, a number or a word.
  • You can read a chart by keyboard: the arrow keys walk through the categories, and a screen reader reads out the tooltip.
  • Below 768 px every column of a table stays within reach; nothing is dropped.

Every departure from the house style is written down, with the reason.

Tested in a real browser

Unit tests run in happy-dom. The rest runs in a real browser, through Vitest and Playwright. Every example in the style guide is drawn in light and dark, at 1440 and 390 px wide, and has to pass four checks: its tags exist, nothing lands in console.error, every icon has a file, and axe finds no WCAG A or AA violation. CI runs that on every pull request in Chromium, WebKit and Firefox, and once more before every release.

That’s not a full audit. Axe doesn’t look at a page the way a person does, and it doesn’t listen along with a screen reader. One finding is still open and tracked as an issue; fix it without removing the note and the test fails. But it does mean a regression in contrast or labelling shows up before it goes live.

The page stays in charge

One rule shapes the whole API: an element never fetches data, never reads the URL and never keeps timers for the server. When an element wants something, it sends an event; the page answers by setting a property.

That makes it pleasantly boring to use. A page rendered on the server — with PHP, Python or as a static file — needs no script of its own: links are just links, inputs post in a normal <form>, and a chart’s data sits in the element as a JSON script. If your page does have its own JavaScript, it just listens for the events.

Released

lintje is out, as version 0.1. It’s on npm as lintje, and the code is on GitHub under the EUPL-1.2 licence. The style guide and the example applications — with made-up data — are on GitHub Pages, so you can click through all of it without building anything. It also comes with a skill for an AI assistant that builds an application with it.

It’s still 0.x: until 1.0, tags, properties and events can still change. The changelog lists every change that breaks something.

To be clear: lintje is not an official Rijksoverheid product and has no connection to it. The house style and the emblems belong to the Rijksoverheid and the organizations they represent, and only organizations that are allowed to carry them use them. lintje provides the code, not that right.

Aan componenten voor overheidssites geen gebrek. NL Design System en de set van RVO geven je knoppen, formuliervelden, meldingen en headers, netjes in de huisstijl. Voor een website heb je daarmee het meeste wel.

Maar ik wilde applicaties bouwen, met onder meer een dashboard. En daarvoor mist er nogal wat.

Gemaakt voor contentwebsites

De meeste componentbibliotheken — die van de overheid, maar ook veel andere — zijn bedoeld voor eenvoudige contentwebsites: een pagina met tekst, een formulier, een header en een footer. Daar zijn ze goed in. Maar een applicatie heeft meer nodig: een datatabel waarin je kunt sorteren, filteren en bewerken, een drawer, tabs, een stepper, een boomstructuur, een split pane, een multiselect, een datumbereik, toasts en meldingen, een bevestigingsdialoog, een waarschuwing als je sessie bijna verloopt. Niks bijzonders, maar het zit er meestal gewoon niet in.

Voor een dashboard komt daar nog meer bij: grafieken, KPI-tegels, een filterbalk en een navigatie die naast de pagina staat in plaats van erboven.

Waarom niet gewoon een grafiekbibliotheek?

De gebruikelijke oplossing: overheidscomponenten voor de formulieren, en een grafiekbibliotheek voor de rest. Dat werkt, maar je ziet het meteen. Zo’n bibliotheek brengt z’n eigen lettertype, kleuren en tooltip mee, en z’n eigen idee van dark mode. Naast de huisstijl lijkt het een stuk van een andere site. En of je de grafiek met toetsenbord of screenreader kunt lezen, bepaalt de bibliotheek, niet jij.

Neem de datakleuren. De Rijkshuisstijl heeft ze, maar dat weet een grafiekbibliotheek niet. Die weet ook niet dat ze hetzelfde moeten blijven in licht, in donker en in het thema van elke organisatie, zodat een rode staaf overal hetzelfde betekent. Dat kun je beter één keer goed regelen, op één plek, dan op elke pagina opnieuw.

Waarom een verticale navigatie?

Een overheidswebsite heeft z’n navigatie bovenaan: een handvol secties, een submenu per groep, en de pagina scrolt eronder. Een applicatie gebruik je anders. Je springt de hele dag tussen tien, vijftien schermen, op een breed scherm, en je wilt zien waar je bent zonder iets open te klikken. Een menu naast de pagina doet precies dat. Heb je de ruimte nodig, dan klap je het in tot een smalle balk met iconen.

In de huisstijl zit zo’n menu niet — een website heeft het zelden nodig.

Een dark mode die echt af is

Wat ik ook steeds miste: een dark mode die overal is doorgevoerd. Dus niet een donkere achtergrond met daarop een felwitte datumkiezer, of een grafiek die z’n rasterlijnen nog voor een witte pagina tekent. In lintje heeft elk component, elke grafiek en elk thema een donkere versie, en bij elke testrun tekent de styleguide ze allemaal in beide modi.

Donker is ook niet gewoon licht omgedraaid. Het is een eigen kleurenreeks, voor elk thema dezelfde, met eigen contrastregels. Zo krijgt de themakleur in dark mode drie varianten — voor een vlak, een lijn en tekst — omdat elk een andere contrasteis heeft. En een QR-code blijft altijd licht, want camera-apps willen donkere blokjes op een lichte achtergrond.

Beweging die klopt

Ik wilde ook dat de animaties af voelen. Een grafiek tekent z’n data in zodra-ie in beeld komt — één keer; scroll je terug, dan gebeurt het niet opnieuw. Een KPI telt op naar z’n waarde, en een nieuw getal schuift op z’n plek. Wat binnenkomt remt af, wat weggaat versnelt. De hele interface gebruikt één basisduur; databeweging duurt net iets langer.

Twee regels houden het netjes. Een waarde die animeert, staat met z’n eindwaarde in de accessibility tree, zodat een screenreader nooit een tussenstand voorleest. En zet je ‘beweging beperken’ aan, dan verdwijnt er niks behalve de beweging zelf. De focusring beweegt nooit.

Dus bouwde ik de rest

lintje — vernoemd naar het lint in het logo van de Rijksoverheid — is de Rijkshuisstijl als standaard custom elements, gebouwd met Lit 3. Eén script registreert alle lintje-*-tags; op je pagina schrijf je een tag en zet je de attributen.

<link rel="stylesheet" href="dist-elements/tokens.css" />
<link rel="stylesheet" href="dist-elements/fonts.css" />
<script type="module" src="dist-elements/lintje.js"></script>

<lintje-tile heading="Aanvraag">
  <lintje-text-input label="Naam"></lintje-text-input>
  <lintje-button variant="primary">Versturen</lintje-button>
</lintje-tile>

De huisstijl is geschreven voor communicatie

De Rijkshuisstijl regelt de kleuren, het lettertype, het logo en de header van een overheidssite. Maar er staat niet in hoe een geselecteerde tabelrij eruitziet, hoe een grafiek zich in dark mode gedraagt of waar het menu op een telefoon blijft. Voor een applicatie heb je dat allemaal nodig.

lintje vult dat in, in de geest van de huisstijl. Wat er al is, gebruikt het gewoon — RijksSans en de iconen komen rechtstreeks uit de assets van RVO. De rest voegt het toe:

  • Grafieken. Tien soorten, met één vormtaal en zonder grafiekbibliotheek: staaf, liggende staaf, gegroepeerd, gestapeld, lijn, twee assen, taart, heatmap, voortgang naar een doel, en een kaart — van Nederland op PDOK-geometrie, of van de wereld. De datakleuren komen vast uit de Rijkshuisstijl.
  • Twee navigatie-layouts. De bekende balk bovenaan, en een layout met het menu aan de zijkant: vast in beeld, of als smalle balk die over de inhoud uitklapt. Onder de 768 px worden ze allebei een header met een menu dat het hele scherm vult.
  • De onderdelen van een dashboard. Een shell met menu, een paginakop, een filterbalk, KPI-tegels, een datatabel waarin je cellen kunt bewerken. Bij elkaar ruim honderd elementen, verdeeld over dertien categorieën, van het paginakader tot een chat met je data.
  • Zes thema’s. Een thema stelt maar twee properties in, --primary en --accent; de rest wordt daarvan afgeleid met color-mix() en OKLCH, in licht en in donker.

Ontworpen in Claude Design

Het hele ontwerp — de componenten, hun states, de dark mode en de layouts voor de telefoon — is gemaakt in Claude Design, en de code volgt dat ontwerp. De styleguide in de repo laat elk element zien in al z’n states, met de properties en events eronder. Zo kijk je naar ontwerp en code tegelijk.

WCAG AA als ondergrens

Botsen de huisstijl en WCAG AA, dan wint AA — met de kleinste aanpassing die nodig is. Een paar voorbeelden:

  • Kleur is nooit het enige wat iets betekent. Naast een kleur staat altijd een symbool, een getal of een woord.
  • Je kunt een grafiek met het toetsenbord lezen: met de pijltjestoetsen loop je langs de categorieën, en een screenreader leest de tooltip voor.
  • Onder de 768 px blijft elke kolom van een tabel bereikbaar; er wordt niks weggelaten.

Elke afwijking van de huisstijl is vastgelegd, met de reden erbij.

Getest in een echte browser

De unittests draaien in happy-dom. De rest draait in een echte browser, via Vitest en Playwright. Elk voorbeeld uit de styleguide wordt getekend in licht en donker, op 1440 en 390 px breed, en moet aan vier dingen voldoen: de tags bestaan, er komt niks in console.error, elk icoon heeft een bestand, en axe vindt geen overtreding van WCAG A of AA. CI draait dat bij elke pull request in Chromium, WebKit en Firefox, en nog een keer vóór elke release.

Dat is geen volledige audit. Axe kijkt niet naar een pagina zoals een mens dat doet, en luistert niet mee met een screenreader. Eén bevinding staat nog open en is als issue genoteerd; los je die op zonder de notitie weg te halen, dan faalt de test. Maar een achteruitgang in contrast of labels zie je zo wél voordat het live staat.

De pagina houdt de regie

Eén regel bepaalt de hele API: een element haalt nooit zelf data op, leest nooit de URL en houdt geen timers bij voor de server. Wil een element iets, dan stuurt het een event; de pagina antwoordt door een property te zetten.

Daardoor is het lekker saai in gebruik. Een pagina die op de server wordt gerenderd — met PHP, Python of als statisch bestand — heeft geen eigen script nodig: links zijn gewoon links, invoervelden posten in een normaal <form>, en de data voor een grafiek staat als JSON-script in het element. Heeft je pagina wel eigen JavaScript, dan luistert die gewoon naar de events.

Uitgebracht

lintje is uit, als versie 0.1. Het staat op npm als lintje, en de code staat op GitHub onder de EUPL-1.2-licentie. De styleguide en de voorbeeldapplicaties — met verzonnen data — staan op GitHub Pages, dus je kunt overal doorheen klikken zonder iets te bouwen. Er zit ook een skill bij voor een AI-assistent die er een applicatie mee bouwt.

Het is nog 0.x: tot 1.0 kunnen tags, properties en events nog veranderen. In de changelog staat elke wijziging die iets breekt.

Voor de duidelijkheid: lintje is geen officieel product van de Rijksoverheid en heeft er geen band mee. De huisstijl en de emblemen zijn van de Rijksoverheid en van de organisaties die ze vertegenwoordigen, en alleen organisaties die ze mogen voeren, gebruiken ze. lintje levert de code, niet dat recht.

typescriptlitweb-componentswcagdesign-systems

← all writing← alle artikelen