Utvikling av et tilgjengelig designsystem i React med få ressurser

John Melin
Compendium
Published in
11 min readAug 6, 2024

Selv med begrensede midler og uten et dedikert team, er det mulig å utvikle et designsystem som møter høye standarder for universell utforming og skalerbarhet.

Ved å ta i bruk ren CSS, gode arkitekturvalg i React, og moderne testing- og dokumentasjonsverktøy som Storybook, lyktes vi hos kunden å skape et robust og skalerbart designsystem til tross for begrensede ressurser. Prinsippene vi etablerte, som åpen DOM og bruk av headless biblioteker, ga oss fleksibilitet og effektivitet, og sikret samtidig høy tilgjengelighet og støtte for flere merkevarer.

Utfordringer og krav

For å lykkes måtte vi først avdekke alle utfordringene rundt utvikling som vi tidligere hadde erfart i vår tid hos kunden. Disse erfaringene skulle legge grunnlaget for prosjektet med å utvikle et robust og skalerbart designsystem som kunne møte både dagens og fremtidens behov.

  • Applikasjoner i React: Da alle” applikasjoner hos kunden er skrevet i React, så var dette selvfølgelig krav å støtte. For å sørge at systemet vil leve lengst mulig kunne man tenkt at web components er et bedre valg. Men på dette tidspunktet har React dårlig støtte for web components. Dette vil forhåpentligvis snart endres med React 19! 🎆
  • Sjeldne oppgraderinger: Vi hadde tidligere hatt erfaringer med at applikasjoner ikke oppgraderte React og andre andre biblioteker som ble brukt, slik som f.eks bootstrap eller MUI. Dette første til at mange prosjekter lå såpass langt bak at oppgraderinger var svært vanskelig og måtte i flere tilfeller skrives om på nytt. Det var derfor ønskelig å sørge for at fremtidige oppgraderinger skal være så kjapt og enkelt som mulig.
  • Feilbruk av stylede UI biblioteker: Da vi tidligere ikke hadde et designsystem hadde applikasjonene brukt UI biblioteker som MUI og bootstrap. Disse er helt fine hvis man forholder seg til stylingen de gir. De er dog ikke laget for å tillate store endringer på designet. Hvis man overskriver stylingen har man ingen garanti for at klassenavn eller interne strukturer ikke endrer seg i nye versjoner. Når man oppgraderte til nyere versjoner var det derfor svært ofte at stylingen falt sammen.
  • Problemer med tester: Mange eksisterende applikasjoner led av et overflod av tester som var vanskelige å vedlikeholde og ofte overflødige. Disse var også hovedsakelig skrevet i det eldre testrammeverket Enzyme som ble deprecated i React v15 som var med på å hindre oppgraderinger av React. Vi trengte derfor en bedre tilnærming til testing.
  • Lite frontend-kunnskap: Vi hadde relativt få utviklere med spesialisert frontend-kunnskap hos kunden, noe som krevde så enkelt utvikling som mulig med ekstra grundig dokumentasjon og enkle, intuitive api’er.
  • Usikker finansiering: Utvikling i staten kan være utfordrende da all finansiering sikres via statskassa. Man kan aldri være sikker på hvor mye finansiering man får fra år til år og det er mange prosjekter som ønsker å få finansiert sitt prosjekt. Det var derfor viktig å sørge for å lage et system som krevde vedlikehold med minst mulig innsats men samtidig høyest mulig kvalitet.
  • Ingen dedikert designsystem-team: Grunnet finansieringen var det heller derfor ingen mulighet å lage et dedikert team. Men dette var noe vi måtte bidra til i tillegg til prosjektene vi allerede jobbet på.
  • Høye krav til tilgjengelighet: Da designsystemet var ment å bli brukt både på interne og eksterne applikasjoner hadde tilgjengelighet en veldig høy prioritet. Det kan også tenkes at loven om universell utforming på sikt kan gjelde også statlige interne applikasjoner. Vi måtte derfor sikre at alle våre komponenter var tilgjengelige og brukbare for personer med funksjonshemminger, i tråd med WAI-ARIA-standardene.
  • Krav til mange forskjellige temaer (brands): Kunden har flere “samarbeidspartnere”. Det var derfor ønskelig å kunne bruke det samme designsystemet på produkter med andre brands enn kundens egne.

Etablering av prinsipper

For å møte krav og utfordringene, etablert vi flere prinsipper og retningslinjer.

1. Ren CSS for styling

Siden vi hadde få ressurser og tidligere erfaringer med at systemer ikke oppgrader bibliotekene sine ville vi være minst mulig avhengig av eksterne biblioteker som krever stadige oppgraderinger ved nye React versjoner.

React har ikke noen innbygget løsning for å unngå at css klasser overskriver hverandre. I React miljøet er dette løst på en rekke forskjellige måter med løsninger som f.eks forskjellige css-in-js pakker. Disse har de negative sidene at alle krever ekstra oppsett med byggsteg, du er låst til deres måte å style på, kan plutselig bestemme å endre hvordan du skal style, må hele tiden oppgraderes og kan plutselig deprecates.

Ren CSS sammen med CSS modules (for å forhindre klassekollisjoner) til gjengjeld vil neppe forsvinne med det første og har en rekke andre fordeler som passer perfekt i vårt tilfelle.

  • “Alle” kan css: Det er større sannsynlighet at en annen utvikler kan css enn en bestemt variasjon av de mange css-in-js løsningene, Tailwind eller Bootstrap.
  • CSS er uavhengig av rammeverk: Siden ren css ikke er rammeverk spesifikt, kan styling til design systemet på et senere tidspunkt enkelt flyttes over til en annen teknologi som f.eks Web components.
  • CSS krever ingen spesielle bygg steg: Til forskjell fra css-in-js og andre systemer som Tailwind krever ikke css noe bygg steg, eller noe oppsett som helst. Det er bare å sette i gang og vil aldri faile.
  • CSS krever ikke oppgradering: Siden CSS leses av browseren og ikke er avhengig av rammeverket kan du hoppe rett inn og ta i bruk ny css funksjonalitet (så fort alle browsere støtter det).

Komponent styling: Hver React-komponent hadde sin egen CSS-modulfil, som kapslet inn stilene og forhindret lekkasje. Denne isolasjonen sikret at endringer i en komponents stiler ikke påvirket andre komponenter.

Conditional classes: For å lett kunne toggle klasser av og på basert på logiske betingelser brukte vi pakken classNames (clsx er mindre og så godt som en drop in replacement til classNames). Denne pakken gir deg lik styling logikk som rammeverk som Angular og Vue. Den er veldig enkel og er skrevet i ren javascript og vil derfor ikke hindre fremtidige oppgraderinger av React.

Multiple branding/theming: Ved å lene seg på css variabler får vi mulighet å enkelt rebrande hele designsystemet i appen din. Dette funker ved at vi har et predefinert sett med css variabler for farger som defineres i et css stylesheet for hvert brand. Fargene er semantisk navngitt, som vil si at de ikke beskriver hva fargen er men heller hvordan den skal brukes. Slik står vi fritt til å redefinere variabelen til den fargen som brukes for bestemt brand. Vi har også egentlig et sett med globalt definerte farge variabler for å lettere kunne referere til og snakkes om med designer, men det er de semantiske fargene som skal brukes i komponenter og i selve appene.

<main-brand>.css
...
:root {
--design-color-surface-action-primary-default: var(--<main-brand>-color-primary-900);
....
}

<sub-brand>cscc.css
...
:root {
--design-color-surface-action-primary-default: var(--<sub-brand>-color-primary-900);
...
}

Alle komponenter i design systemet referer kun til disse semantiske css variablene og disse css filene eksponeres fra designsystemet. For en app er det dermed bare å importere riktig css fil slik at variablene referer til fargen for ønsket brand.

I designsystemet for f.eks button.css
.button.primary {
background-color: var(--design-color-surface-action-primary-default);
}

For kundens egne app i index.css
@import "@design/ui/<main-brand>.css";

For kundens underleverandør app i index.css
@import "@design/ui/<sub-brand>.css";

2. Åpen DOM prinsipp

Siden vi hadde få ressurser og det samtidig er umulig å forutse hvilke behov konsumentene av designsystemet vil få ble komponentene våre utviklet med et prinsipp om å bruke “åpen DOM”. Det vil si at vi ikke ønsker å ha en prop eller flagg for alle mulige kombinasjoner av varianter, men heller gi konsumenten direkte tilgang til “innholdet” i komponentene. Å bruke enkelt-props tar ikke bare tid å utvikle, men vil over tid gi vanskeligere og vanskeligere kode å vedlikeholde som igjen øker risikoen for bugs.

Som et konkret eksempel ville vi ikke ha en “text prop” på <Button />, men heller la konsumenten sette teksten slik <Button>Tittel</Button>. Dette kan i React gjøres ved å bruke “children”.

const Button = (props) => {
return (<button>{props.children}</button>);
}

På den måten har konsumenten mulighet f.eks style en bestemt bokstav i teksten. Konsumenten kan også bruke valgfritt ikon fra hvor de vil og plassere ikonet hvor hen de ønsker.

Fleksibilitet vs feilbruk

Det kan dog være tilfeller der man faktisk vil begrense hvordan komponenten kan brukes. Fleksibilitet det gir ved å bruke åpen DOM også gjøre lettere for feilbruk. Det kunne f.eks i forrige eksempel likevel være en idé å begrense bruken av children fordi det kan åpne opp for at konsumenten setter ikoner på begge sider av teksten. Etter erfaring er det som oftest likevel å foretrekke da konsumenter i mange tilfeller ender opp å tvinge komponentene til å gjøre det de vil uansett.

Compound components

Bruken av children i React er en ganske vanlig og velkjent teknikk. Men hva med mer avansert eksempler enn Button? Hva hvis jeg ønsker å lage en Modal? Hvordan kan jeg jeg eksponere header, innhold og knapper?

Dette kan enkelt gjøres ved bruk av teknikken Compound components. Det lar deg enkelt skrive komponenter på denne måten. Dette speiler api’et til native html og gjør komponentene svært fleksible. Siden elementene er eksponert, kan man enkelt style de enkeltvis slik man ønsker og hvilket innhold man vil, hvorhen man vil.

<Modal> 
<Modal.Header>Tittel og slikt</Modal.Header>

<Modal.Content>Innholdet i modalen</Modal.Content>

<Modal.ActionButtons>
<Button>Avbryt</Button>
<Button>Primærhandling</Button>
</Modal.ActionButtons>
</Modal>

Det finnes flust av gode artikler som graver dypere i temaet. Kort sagt benytter teknikken seg av “children”, men bruker muligheten til å plukke ut enkelt elementer i children slik at man kan plassere innhold der man vil og få autocomplete på hvilke komponenter som hører til. De er også isolert til parent-komponenten og kan ikke brukes utenfor.

export function findChild<T>(children: ReactNode, type: T): ReactElement<T> {
const childrenArray = Children.toArray(children);
const child = childrenArray.find(
(child) => isValidElement(child) && child.type === type,
) as ReactElement<T>;
return child;
}

export const Modal = ({ children }) => {
const header = findChild(children, Modal.Header);
const content = findChild(children, Modal.Content);
const actionButtons = findChild(children, Modal.ActionButtons);

return (
<dialog>
{header}
{content}
{actionButtons}
</dialog>
);
}

Modal.Header = ({ children }) => <header><h1>{children}</h1></header>
Modal.Content = ({ children }) => <div>{children}</div>
Modal.ActionButtons = ({ children }) => <div>{children}</div>

forwardRef og eksporter eksisterende props sammen med custom Props.

En av fordelene med React er hvor enkelt det er å eksponere native html elementer og elementets props. Dette gjør at man ved å bruke forwardRef funksjonen. Du får da en “ref” attributt som du kan knytte til elementet du eksponerer.

type Props = ComponentProps<"input"> & {
// andre custom props
}

const Input = forwardRef<HTMLInputElement, Props>(props, ref) => {
reutrn (<input {...props} ref={ref} />);
}

Dette gjør det f.eks mulig å programmatisk kalle på funksjoner som focus, blur etc. Det er nødvendig for biblioteker som react-hook-form til å fungere sømløst uten å måtte sette opp onChange og value props manuelt.

Ved å legge ved typen ComponentProps<”element-navn”> i props definisjonen får du med alle mulige props for gitte elementet slik at du slipper å definere hver eneste mulig prop.

3. Headless biblioteker for økt hastighet og universell utforming

I et første prinsipp nevnte jeg at vi ønsket å minimere bruken av eksterne biblioteker der det ikke er nødvendig. Samtidig hadde vi som nevnt begrenset med ressurser og ville ha en høy hastighet på utviklingen og hadde høye krav til tilgjengelighet, men hadde som nevnt opplevd problemer med å style biblioteker som Bootstrap og MUI.

Heldigvis finnes det flere gode biblioteker som Radix-UI, Headless UI and Tanstack Table som tillot oss å lage konsistente og tilgjengelige UI-komponenter. Disse kommer altså uten noen form for styling og siden disse også benytter seg av åpen dom prinsipp og compound components teknikken lot de seg enkelt style med våre CSS-moduler.

Native html for enkle komponenter

For å redusere avhengigheter til eksterne biblioteker forsøkte vi fortsatt å bygge en del komponenter i plain html når vi anså det som “enkelt nok”. Slik som f.eks button, input felt, checkboxes, radiobuttons, autocomplete og alerts.

4. Dokumentasjon skal være lettbrukt og testing skal være skalerbart

Som nevnt tidligere hadde vi ikke mulighet for et dedikert team for designsystemet. Vi var derfor avhengige av bidrag fra utviklere fra forskjellige prosjekter på huset. For at det skulle være lettest mulig å både bidra samt å bruke designsystemet er god lettilgjengelig dokumentasjon nødvendig.

Dokumentasjon bundlet med koden

Dette ble gjort ved defacto-standard dokumentasjons-verktøyet Storybook som lar deg legge ut live kodeeksempler av komponentene dine, samt utvikle de i isolasjon. For at dokumentasjonen skulle være lettest tilgjengelig, ble dette bundlet sammen med koden som et monorepo (sammen med andre ting som util funksjoner etc). Dermed så har du dokumentasjonen så lenge du har koden.

I storybook dokumenterte vi alle forskjellige kombinasjoner og varianter du kan forventes å bruke komponentene sammen med gode og riktige forslag til bruk skrevet av designer.

Testing uten å være avhengig av React

Et stort problem vedrørende testing var som tidligere nevnt testing-verktøy som Enzyme. Da rammeverket Enzyme tester React komponenter ved å rendre dem, var det avhengig av å implementere render funksjonen til React. Etter at React endret måten de rendrer komponenter på sluttet dermed Enzyme å fungere. Ja det kom community pakker som gjorde det mulig igjen, men etter versjon 18, sluttet også disse å fungere. Enzyme ble dermed et hinder for å oppgradere versjonen av React. I mange apper på hos kunden hadde man opp til 1000 tusen tester (et problem i seg selv) og man måtte isåfall skrive om alle testene i andre testrammeverk for å ikke miste testdekning.

React testing library er biblioteket som har tatt over for Enzyme, men selv om det er et langt bedre verktøy gjenstår det samme problemet. React testing library er avhengig av Reacts render funksjon for å kjøre komponentene. Vi ønsket derfor å bruke andre metoder å teste på.

En måte er å flytte ut logiske funksjoner utenfor komponenten slik at logikk kan testes uten å rendre komponenten. Disse testene blir dermed rene javascript og typescript tester som ikke er avhengige av React på noen måte.

Men det er av og til også nødvendig å teste selve komponentene. Der har Storybook igjen en fordel. Siden alle variantene er rendret på nettsiden kan man teste disse direkte med verktøy som Cypress og Playwright. Du trenger dermed igjen ikke ta hensyn til React, da du tester igjennom selve nettsiden. Hvis vi på et senere tidspunkt skulle skrevet om til f.eks Web Components så vil fortsatt testene kjøre som før (med små modifikasjoner i tilfelle endringen av Dom struktur).

Storybook har vistnok også tillegg for visuell testing og ga oss varsel om eventuelle feilende tester på universell utforming.

Utviklingen av designsystemet hos kunden var en reise fra utfordringer til muligheter. Ved å ta i bruk ren CSS, gode arkitekturvalg i React, og moderne testing- og dokumentasjons-verktøy som Storybook, lyktes vi i å skape et robust og skalerbart designsystem til tross for begrensede ressurser. Prinsippene vi etablerte, som åpen DOM og bruk av headless biblioteker, ga oss fleksibilitet og effektivitet, og sikret samtidig høy tilgjengelighet og støtte for flere merkevarer.

Vår erfaring viser at selv med begrensede midler og uten et dedikert team, er det mulig å utvikle et designsystem som møter høye standarder for universell utforming og skalerbarhet. Dette ved at man adresserer tidligere svakheter og fokuserer på enkelhet, fleksibilitet og grundig dokumentasjon.

--

--