Designsystemarkitektur · Fristil 0.27.1 · 3. oktober 2026

Rammeverksuavhengig designsystem

Et tekstfelt med ledetekst, hjelpetekst og feilmelding er HTML og ARIA, og har vært det lenger enn noe rammeverk i bruk i dag. Likevel koster det nå et rammeverk du ikke kan bytte ut, og en omskriving hver gang det rammeverket skifter retning.

Dette er syv arkitektoniske beslutninger som gjør det unødvendig. For hver av dem: hva den går ut på, hva den koster, og hvor den ikke holder. Ingen av dem er gratis, og prisen står nederst på lysbildene som tar et standpunkt.

Alt henger på denne ene setningen:

Markupen eies av den som sender den. Oppførselen eies av komponenten.

Fristil, bygget og publisert på fristil.netlify.app og på npm som @fristil/designsystem.

TL;DR

Rammeverk kommer og går. Et designsystem som er bundet til ett av dem, må skrives om hver gang noe skifter. Derfor er dette bygget på det nettleseren allerede forstår.

  1. Nettleseren er plattformen. Komponentene er CSS-klasser og web components, og virker likt i React, Astro, ren HTML og i en Kotlin-server.
  2. To lag som ikke overlapper. Serveren skriver klasser og koblinger, komponenten tar tastatur, fokus og posisjonering.
  3. Tilgjengelighet er skrevet én gang. Den samme funksjonen regner ut koblingene, enten den kalles av serveren eller av elementet i nettleseren.
  4. Teamene kan overstyre alt utenfra, og overta en komponent helt hvis de må.
  5. Arbeidet flyttes, det forsvinner ikke. Tre nettlesermotorer i hver testkjøring, venting på den siste som mangler en funksjon, og klassenavn vi ikke kan røre fordi de står i koden til andre.

Nederst på lysbildene står to linjer: hva punktet betyr for teamene som bruker systemet, og hva det koster den som forvalter det.

Slik ser det ut å bruke det

Før vi går inn i arkitekturen: dette er hele bruken. Du installerer én pakke, importerer stilarket for det du trenger, og skriver vanlig HTML.

Bare klasser: ingen JavaScript i det hele tatt

import "@fristil/designsystem/tokens.css"
import "@fristil/designsystem/field.css"

<label class="fs-label">E-post</label>
<input class="fs-input" type="email">
<p class="fs-error-text">Skriv en gyldig adresse</p>

Samme markup, med ett element rundt

import { defineFsField } from "@fristil/designsystem/field"
defineFsField()

<fs-field invalid>
  <label class="fs-label">E-post</label>
  <input class="fs-input" type="email">
  <p class="fs-error-text">Skriv en gyldig adresse</p>
</fs-field>

Til venstre kjører det ingen kode. Ledeteksten, feltet og feilmeldingen er vanlige HTML-elementer med klasser fra systemet, og de ser riktige ut med én gang. Til høyre står nøyaktig de samme elementene, med ett element rundt. Det er <fs-field> som setter id, for og aria-describedby, slik at en skjermleser leser feltet med både ledetekst og feilmelding.

ProduktteameneKan begynne med tokens og noen knapper, og ta resten når de trenger det.

DesignsystemteametMå holde denne enkelheten, også når en komponent blir vanskelig innvendig.

Syv arkitektoniske beslutninger

Resten av presentasjonen tar én beslutning om gangen, med begrunnelsen og hva den fører til. De henger sammen: den første gjør de andre mulige, og den siste er garantien for at du kan gå fra alt sammen hvis du vil, når du vil.

  1. Nettleseren er plattformen5 lysbilder
  2. Serveren eier markupen4 lysbilder
  3. Tilgjengeligheten skrives ett sted1 lysbilde
  4. DOM-en er fasiten3 lysbilder
  5. Typesikkerhet uten rammeverk1 lysbilde
  6. Konsumentens CSS vinner3 lysbilder
  7. Du kan ta over koden1 lysbilde
Beslutning 1 av 7

Nettleseren er plattformen

Systemet bygger bare på det nettleseren allerede kan. Ingen kjøretid for utseende, ingen avhengigheter, og ingenting som må skrives om når appene rundt bytter rammeverk.

  1. Hvorfor plattformen først, og ikke React først
  2. Hva nettleseren gjør for oss
  3. Prisen for å bygge på nettleseren selv
  4. Hva det koster appen din
  5. Hva én knapp legger i node_modules

Beslutning 1 av 7 · Nettleseren er plattformen

Hvorfor plattformen først, og ikke React først

Vi valgte ikke bort React fordi noe annet er bedre i dag. Vi valgte det bort fordi appene rundt systemet kommer til å byttes ut, og systemet skal stå igjen.

ProduktteameneReact-utviklerne får ikke ferdige React-komponenter, men klasser og attributter, med typer og en egen React-inngang.

DesignsystemteametMå gjøre uavhengigheten usynlig i hverdagen, ellers oppleves den som et steg ned fra et React-bibliotek.

Beslutning 1 av 7 · Nettleseren er plattformen

Hva nettleseren gjør for oss

Det meste av det som er vanskelig i et designsystem, er allerede løst i nettleseren. Bygger vi på det, er det ikke kode vi må vedlikeholde.

Det nettleseren tar

  • Fokusfellen i en dialog. showModal() gjør resten av siden utilgjengelig, holder fokus inne, og lukker på Escape.
  • Topplaget. En dialog og et sprettoppvindu ligger over alt annet uten at noen teller z-index.
  • Skjemaet. Validering, innsending og :invalid finnes, og virker uten at vi skriver en linje.
  • Utseendet. Farge, form og avstand er CSS, og står ferdig før noe skript har rukket å kjøre.

Det vi slipper å eie

  • Ingen egen fokusfelle, ingen egen z-index-stige, ingen egen skjemamotor.
  • Ingen kjøretid for det som bare er utseende.
  • Ingen omskriving når appene rundt bytter rammeverk.
  • Ingen versjon av React, Vue eller Svelte å holde tritt med.

Igjen står koblingen mellom delene, tastaturet der nettleseren ikke har en ferdig kontroll, og at alt ser likt ut. Det er en langt mindre jobb enn å bygge fokus, lag og skjemaer selv.

Beslutning 1 av 7 · Nettleseren er plattformen

Prisen for å bygge på nettleseren selv

Uavhengigheten er ikke gratis, den er bare betalt et annet sted: av oss som lager systemet, ikke av teamene som bruker det. Her er hva den koster.

3

nettlesermotorer i hver testkjøring i CI: Chromium, Firefox og WebKit, med rundt 1 150 tester i hver og nær 3 500 til sammen. Hele premisset er at vi bruker nettleserens egne API-er, og det er nettopp de som oppfører seg ulikt fra motor til motor.

Venting på den siste motoren

Vi krever ikke at alle tre motorene har funksjonen. Vi krever at motoren som mangler den gir noe som virker.

appearance: base-select
Chromium 148 ✓  WebKit 26.4 ✓
Firefox 150     ✗

Derfor er den valgfri, med data-picker="styled". Uten støtte får du nettleserens egen nedtrekksliste, altså nøyaktig det feltet hadde før.

Navn som ikke kan døpes om

Klassenavn, data-*, tokennavn og oppføringer i exports er alt sammen noe en konsument bygger på. Et brudd på dem krever ny hovedversjon, og står i versjonsloggen i den samme pull requesten som endringen.

Beslutning 1 av 7 · Nettleseren er plattformen

Hva det koster appen din

Et designsystem stiller krav til appen som tar det i bruk. Her er kravene våre, og til sammenligning kravene til et React-basert designsystem, hentet fra peerDependencies i @skatteetaten/ds-forms 3.0.0:

For å bruke skjemakomponentene deres må appen din ha tolv pakker: React 19, i18next, react-i18next, date-fns, og åtte pakker fra systemet selv. Hos oss er svaret ingen, og hvilket rammeverk du bruker spiller ingen rolle.

Dette er ingen kritikk. Et React-basert system er et rimelig valg når alt er React. Spørsmålet er hva det koster den som ikke er det.

Én knapp på skjermen, i en tom app. Bygget med esbuild, gzippet.

Det som må lastesFristilEt React-basert system
JavaScript0 kB, knappen er en klasse119,0 kB
CSS1,9 kB4,3 kB
Til sammen1,9 kB123,3 kB

Av de 119 kilobytene er 67 React selv, så har du React fra før legger designsystemet deres til rundt 51 kB for den ene knappen. Vil du ha byggefunksjonen med typer i tillegg, koster fs 3,9 kB hos oss.

Pakken har null avhengigheter, og ingenting i den gjør noe ved import alene. Hver modul kan lastes på en server uten DOM, så import { fs } virker med server-rendring.

Beslutning 1 av 7 · Nettleseren er plattformen

Hva én knapp legger i node_modules

En React- og TypeScript-app som har React 19 fra før, i fem kopier. I hver installeres det dokumentasjonen sier for én knapp, og så leses package-lock.json. Lengst til høyre i grafen står det alt hviler på.

Designsystemets egne pakker Pakker det drar inn Har appen fra før Avhengighet Peer-avhengighet

Målt 26. september 2026, mot versjonene som da var nyeste. Aksel og Digdir har ingen egen knappepakke, så knappen koster hele React-pakken. Fristil er samme ene pakke med klassene som med byggefunksjonen: importen avgjør hva som lastes, ikke installasjonen. «CSS til knappen» er det som må lastes for én knapp i CSS-sporet, tokens og knapp, minifisert og gzippet som på forrige lysbilde. Stiplet er peer-avhengigheter, som npm 7 og nyere henter selv.

Beslutning 2 av 7

Serveren eier markupen, komponenten eier oppførselen

Den som sender HTML-en bestemmer hva som står der. Komponenten fester tastatur, fokus og koblinger på den, og legger aldri til noder eller attributter serveren eier. Av samme grunn gjemmer vi den ikke i en shadow root.

  1. Hva server-rendring krever av en komponent
  2. De to lagene
  3. Tre kategorier, og ett spørsmål
  4. To veier til den samme kontrakten

Beslutning 2 av 7 · Serveren eier markupen

Hva server-rendring krever av en komponent

Systemet skal virke i apper som bygger HTML-en på serveren, altså server-rendring (SSR). Når en slik app oppdaterer noe, sender serveren den samme biten HTML på nytt, og et bibliotek fletter den inn i siden. Det kalles morfing, på engelsk DOM morphing. Under er de to linjene i Datastar som gjør det, og de er grunnen til at systemet er bygget som det er.

datastar.js 1.0.4, løkka som går gjennom attributtene ved hver oppdatering

// r = elementet i siden nå, s = det serveren sendte,
// o = navnene i data-preserve-attr. Like før er s kopiert inn i r.
for (let {name: l} of Array.from(r.attributes))
  !s.hasAttribute(l) && !o.includes(l) && r.removeAttribute(l)

// Står attributtet der, men ikke i det serveren sendte,
// så fjern det. Med mindre malen har fredet navnet.
Utgang:
E-post

Nettleseren, elementet r

Til og fra serveren, elementet s

Nødutgangen er data-preserve-attr, som HTML-malen på serveren må fylle med navnene på alt komponenten har satt. Endrer vi hva komponenten setter, må hver mal endres i takt, uten at noen kompilator sier fra. Derfor bygger vi ikke på den.

Har en komponent lagt til aria-describedby i nettleseren, er det borte ved neste oppdatering, og skjermleseren mister koblingen uten at noe sier fra. React har det samme i en annen form: rendrer foreldrekomponenten på nytt, kan noder en web component har lagt inn bli kastet. Derfor:

En komponent skal aldri legge til noder eller attributter på DOM som serveren eier.

Hver komponent skal virke i en app med server-rendret HTML, i TanStack Start, i Datastar og i React Server Components. Datastars morfing er den strengeste, så det er den vi bygger etter. Kravet er likevel formulert uten navn: det komponenten har satt, skal overleve at attributter blir fjernet.

Beslutning 2 av 7 · Serveren eier markupen

De to lagene

Følgen av regelen på forrige lysbilde er at vi deler systemet i to lag: det serveren skriver, og det nettleseren gjør etterpå. Delingen er nødvendig fordi en oppdatering river bort attributter og noder som ikke sto i det serveren sendte, mens hendelseslyttere blir stående. De to lagene overlapper derfor ikke: en byggefunksjon setter aldri en hendelseslytter, og en komponent setter aldri et attributt på noe serveren eier.

LagInnholdHvem produserer det
fs-byggefunksjoneneKlasser, data-*, tilgjengelighetskoblingenServeren, i hvilket som helst språk
Web componentsTastatur, fokus, posisjoneringNettleseren, etter at HTML-en står der

Ingen shadow DOM. Det er grunnen til at mange har prøvd web components og gitt opp: et felt i en shadow root står utenfor skjemaet rundt, og du må melde verdien inn fra verten med ElementInternals for å få den med i innsendingen. Her skriver du <input> selv, i vanlig DOM, og komponenten legger seg rundt.

Beslutning 2 av 7 · Serveren eier markupen

Tre kategorier, og ett spørsmål

I en app med server-rendring skriver serveren all HTML uansett, så kategorien handler ikke om det. Den handler om hvem som eier DOM-en mens siden lever.

css

Ingen eier noe. Markupen er statisk: en klasse og noen data-*.

<button class="fs-button"
  data-variant="secondary">
ramme

Serveren eier markupen. Komponenten lytter, og rører verken noder eller attributter.

<fs-field>
  <label>…</label>
  <input class="fs-input">
</fs-field>
frittstående

Tre elementer: meldingen, varselet om at økten løper ut, og linja som sier fra når forbindelsen ryker. De bærer ingen verdi inn i en innsending, og innholdet lager de selv, når du kaller dem.

toast.show("Lagret")

Én setning avgjør hvor en komponent havner.

En komponent som bærer en verdi inn i en innsending, eller som har innhold serveren må sende, kan ikke eie sin egen markup. Den hører under ramme.

Beslutning 2 av 7 · Serveren eier markupen

To veier til den samme kontrakten

Det avgjørende er om koden som lager markupen kan kalle en funksjon. Hvor den kjører spiller ingen rolle.

Koden kan kalle en funksjon: React, Astro, Node

const felt = fs.field({ id: useId(), help: true })

<div>
  <label {...felt.label}>E-post</label>
  <input {...felt.control} type="email" />
  <p {...felt.help}>Vi sender aldri spam</p>
</div>

// Koblingen står i markupen serveren sendte.
// Komponenten trengs ikke.

Markupen blir til uten JavaScript: Go, PHP, Kotlin, håndskrevet

<fs-field>
  <label class="fs-label">E-post</label>
  <input class="fs-input" type="email">
  <p class="fs-help-text">Vi sender aldri spam</p>
</fs-field>

// Ingen id-er, ingen for, ingen aria-describedby.
// Komponenten setter koblingen i nettleseren.

De to er ikke to smaksvalg. De er to veier for to slags kode, og begge ender med den samme HTML-en hos brukeren.

ProduktteameneTrenger ikke vite hvilken vei som gjelder før de vet hvor markupen lages.

DesignsystemteametMå holde de to veiene i takt, for begge kaller den samme funksjonen innvendig.

Beslutning 3 av 7

Tilgjengeligheten skrives ett sted, og nås fra to

Reglene for hvordan et skjemafelt kobles sammen for en skjermleser finnes ett sted i koden, og både serveren og komponenten kaller det samme stedet.

  1. Én funksjon, to innganger

Beslutning 3 av 7 · Tilgjengeligheten skrives ett sted

Én funksjon, to innganger

Koblingen mellom ledetekst, felt, hjelpetekst og feilmelding er den samme uansett hvem som skriver markupen. Derfor finnes den ett sted, og de to veiene er to kall til den samme funksjonen.

ramme/field/field-core.ts

computeFieldAttributes(...)
   ├── fs.field()      ← serveren kaller den direkte
   └── <fs-field>      ← komponenten kaller den på elementer som står i DOM-en

Endrer vi kontrakten, endrer vi den der, ikke i komponenten. Det svarer også på spørsmålet som alltid kommer. Skal ikke hver server skrive dette selv? Den dagen de må, er Fristil et JavaScript-designsystem med en manuell reserve for alle andre, og det er reserven som ryker først.

En id som lages av seg selv, overlever ikke hydrering. Serveren og nettleseren lager da hver sin, og koblingen er brutt til React har rettet den opp. Derfor er id påkrevd i typen, med en melding i konsollen ved siden av for dem typen ikke når.

En sjekk som må slutte fra miljøet til hva utvikleren mente, gjetter feil i minst ett av dem. Lukk fella i typen framfor å gjette.

Beslutning 4 av 7

DOM-en er fasiten

Finnes tilstanden i markupen, leser komponenten den derfra. Den holder aldri en egen kopi av noe serveren også kan skrive, og setter tilbake det en oppdatering river bort.

  1. Når serveren river bort det komponenten satte
  2. Komponenten skal aldri regne ut det den kan lese
  3. Komponenten sier fra når markupen ikke henger sammen

Beslutning 4 av 7 · DOM-en er fasiten

Når serveren river bort det komponenten satte

Oppdateringen fjerner det serveren ikke sendte. Komponenten vet hva den selv har regnet ut, og skriver det inn igjen med én gang. Malen trenger derfor ikke vite noe om hva komponenten gjør:

Malen, i et hvilket som helst språk

<fs-field>
  <label class="fs-label">E-post</label>
  <input class="fs-input" type="email">
  <p class="fs-help-text">Vi sender aldri spam</p>
</fs-field>

Ingen id-er, ingen for, ingen aria-describedby, og ingen liste over hva som må bevares. Komponenten setter koblingen, og setter den tilbake hver gang den forsvinner.

Vil serveren eie tilstanden selv

<fs-field server-controlled>

Ett attributt på verten, og komponenten reparerer ingenting. Da er det serverens utgave som gjelder, hver gang.

Har komponenten regnet ut attributtet, kan den regne det ut på nytt. Har brukeren gjort noe, vet komponenten hva det var, for den var til stede da det skjedde. Det er det som gjør reparasjonen mulig.

Det flimrer ikke, og det er testet. Oppdateringen fjerner attributtet, observatøren setter det tilbake i samme mikrotask, og nettleseren rekker ikke å tegne imellom. Serverens utgave står i DOM-en et øyeblikk uten at noen ser den.

Beslutning 4 av 7 · DOM-en er fasiten

Komponenten skal aldri regne ut det den kan lese

Serveren og komponenten ser på den samme siden. Holder komponenten sin egen utgave av noe serveren også kan skrive, finnes svaret to steder, og da er det bare et spørsmål om tid før de to er uenige.

Et felt som er ugyldig, står som aria-invalid på kontrollen. Leser komponenten det derfra, er den alltid enig med serveren. Leser den i stedet av sitt eget attributt på verten, vil den før eller siden fjerne et svar serveren nettopp skrev: i React som en hydreringsfeil, i Datastar som en feilmelding på et gyldig felt.

Finnes tilstanden i markupen, skal komponenten lese den derfra. En komponent skal aldri ha en parallell utgave av noe serveren også kan skrive.

Hvilken fane som er valgt leses derfor fra aria-selected og ikke fra et eget attributt. Et attributt komponenten både leser og skriver må i tillegg vite hvem som satte det.

Noter verdien komponenten skrev, og på hvilken node. Står det noe annet der neste gang, har noen andre rørt det. Utled aldri eierskap av hva komponenten trodde sist.

Uten det gir den samme markupen to ulike svar, alt etter hva verten har vært innom før.

Beslutning 4 av 7 · DOM-en er fasiten

Komponenten sier fra når markupen ikke henger sammen

HTML har ingen kompilator. Skriver et team et felt uten ledetekst, er det ingenting som stopper dem, og siden ser helt riktig ut.

Den som merker feilen er den som ikke ser skjermen: feltet blir lest opp uten navn. Tidligere gjorde komponenten ingenting i slike tilfeller, og den sa heller ingenting. Nå skriver den én linje i konsollen, mens utvikleren fortsatt sitter med koden.

Markupen som ble skrevet

<fs-field>
  <input class="fs-input" id="fodselsdato">
</fs-field>

Det utvikleren får se

fs-field: fant ingen <label>.
Feltet får da ingen ledetekst, og
en skjermleser leser det opp uten navn.

En advarsel som gir falsk alarm blir slått av i løpet av en uke. Tre valg gjør at denne er til å stole på:

Meldingene kan ikke råtne: en sjekk leser konsollen på hver bygde dokumentasjonsside, og en advarsel som slår ut på våre egne eksempler feller bygget.

Beslutning 5 av 7

Typesikkerhet uten rammeverk

Du skal ikke miste autofullføring og røde streker i editoren fordi du forlot React. Byggefunksjonene er vanlige TypeScript-funksjoner som returnerer attributter.

Det er dette som gjør at rammeverksuavhengighet ikke oppleves som et steg ned.

  1. Ett valgobjekt inn, attributter ut

Beslutning 5 av 7 · Typesikkerhet uten rammeverk

Ett valgobjekt inn, attributter ut

Innvendingen mot å forlate React er som regel utvikleropplevelsen: du mister komponenter som kjenner sine egne valg. Svaret er at en byggefunksjon gir det samme, fordi den er en vanlig TypeScript-funksjon. Den returnerer attributter, og editoren kjenner dem.

Kallet, og det som kommer ut

fs.button({ variant: "secondary" })
// { class: "fs-button", "data-variant": "secondary" }

fs.input({ type: "email", state: "invalid" })
// { class: "fs-input", type: "email",
//   "data-state": "invalid", "aria-invalid": "true" }

Formen er fast

  • Alltid et objekt, aldri posisjonelle argumenter.
  • Standardverdien gir ikke noe attributt. CSS-en har den allerede.
  • Lovlige verdier henges på funksjonen, så fs.button.variants dukker opp i editoren.
  • Ett begrep, én betydning: variant er visuell vekt, color er hva en status betyr, state er validering.

Skriver du fs.button({ variant: "sekundr" }), får du rød strek i editoren før du lagrer, overalt der TypeScript ser koden: i JSX, i Astro, og i JavaScript med typesjekking slått på. Uten typesjekking finnes svaret ved kjøring i stedet, som fs.button.isVariant("sekundr"). src/react.ts er den samme fs med className og htmlFor, slik at React ikke advarer i konsollen.

ProduktteameneFår typene uten å installere et rammeverk, og uten at vi har skrevet en komponent for dem.

DesignsystemteametMå holde lovlige verdier, typene og CSS-en i takt. En verdi som ikke gjør noe er verre enn ingen verdi.

Beslutning 6 av 7

Konsumentens CSS vinner

All komponent-CSS ligger i et eget lag. En helt vanlig regel skrevet av teamet slår våre, uten !important og uten spesifisitetstriks.

  1. Laget som gjør det mulig
  2. Fargene holder kontrastkravet
  3. Terskelen følger rollen, ikke fargen

Beslutning 6 av 7 · Konsumentens CSS vinner

Laget som gjør det mulig

Et team som må skrive !important for å flytte en knapp fire piksler, slutter å bruke systemet. Derfor ligger all komponent-CSS i @layer fristil, og en regel uten lag slår alt som står i ett.

Uten laget taper du med lik logikk

.fs-button           (0,1,0)
.fs-button:disabled  (0,2,0)  ← vår vinner

Med laget vinner din regel uansett

@layer fristil {
  .fs-button:disabled { … }
}
.fs-button { … }  ← usortert, vinner

Form og størrelse leses fra en variabel

padding: var(--fs-button-padding,
             var(--size-2) var(--size-4));

Standardverdien følger størrelsesskalaen, også når konsumenten endrer --size-*. Vi bruker med vilje ikke @property på disse: en registrert startverdi blir verdien når variabelen ikke er satt, og da når var() aldri reserven.

ProduktteameneKan overstyre alt fra sin egen CSS, uten å kopiere koden vår.

DesignsystemteametMå teste laget og variablene i nettleser, ikke anta at de virker. Begge deler har egne tester.

Beslutning 6 av 7 · Konsumentens CSS vinner

Fargene holder kontrastkravet

Et team med egne merkefarger skal ikke måtte regne ut kontrast for hånd, og skal ikke kunne velge en kombinasjon som faller under kravet. Generatoren tar merkefargene og lager hele temaet, i lyst og mørkt.

Ett kall, eller én kommando

npx @fristil/designsystem tema \
  --aksent=#1362ae --fare=#a82e39 \
  --suksess=#2b6940 --advarsel=#9f7509 \
  --noytral=#757575 --ut=tema.css

Skalaene lages i OKLCH, fordi lyshet i HSL betyr noe ulikt for hver kulør. Merkefargen gir kulør og metning, mens lysheten settes av trinnet. Ellers kan ikke kontrasten garanteres.

Det generatoren svarer med

  • alle --semantic-* for lyst og mørkt tema
  • en liste over hvert trinn den måtte justere for å nå kontrastkravet, med verdien før og etter
  • en liste over det den ikke klarte, framfor å levere noe som ser riktig ut

Ingen skal måtte holde et regneark over fargepar ved siden av paletten sin.

ProduktteameneKan ikke velge en kombinasjon som faller under kravet, for den finnes ikke i temaet.

DesignsystemteametMå holde lista over par oppdatert. Et par ingen har tenkt på, blir ikke kontrollert.

Beslutning 6 av 7 · Konsumentens CSS vinner

Terskelen følger rollen, ikke fargen

Regnestykket er enkelt nok. Det vanskelige er at kravet endrer seg med hva fargen brukes til, så den samme verdien kan bestå ett sted og stryke et annet i den samme paletten.

Det fargen erKravHvor det står
Brødtekst og ledetekster4,5:1WCAG 1.4.3
Stor tekst3:1WCAG 1.4.3
Ramme eller ikon som bærer betydning3:1WCAG 1.4.11
Avslått kontrollunntattWCAG 1.4.3
To flater ved siden av hverandreingen krav

Det er lett å ta feil av, også for den som skrev regnestykket. Et token som heter -foreground kan vise seg å være en ramme og et ikonfyll der det brukes, og da er kravet 3:1. Navnet er ikke et bevis på bruken.

Lista i src/testing/kontrast.ts dekker parene som må holde 4,5:1, og deles av testen for våre egne farger og testen for et generert tema. Den ene 3:1-rollen vi sjekker, feltrammen, står utenfor lista. Hover-rammen ved siden av den sjekkes ingen steder, og ikonene er bilder, så de kan ikke sjekkes av dette i det hele tatt. Hullet dette lysbildet handler om har vi altså selv.

ProduktteameneTrenger ikke vite hvilken paragraf som gjelder for hvilken farge.

DesignsystemteametMå vite det for hvert eneste par, og ta feil i full offentlighet når vi gjør det.

Beslutning 7 av 7

Du kan ta over koden og gå din egen vei

Et designsystem dekker aldri alt. Framfor at teamet skriver et omslag rundt noe som ikke strekker til, kopierer én kommando kildekoden inn i prosjektet deres.

  1. Når systemet ikke strekker til

Beslutning 7 av 7 · Du kan ta over koden

Når systemet ikke strekker til

Et team som trenger noe vi ikke har tenkt på, skal komme videre samme dag. Alternativet er at de kopierer komponenten vår inn i prosjektet for hånd, og da mister vi både kontakten og muligheten til å rette feil.

Kildekoden til én komponent, inn i prosjektet ditt

npx @fristil/designsystem overta button --ut=src/ui

Kommandoen skriver om henvisningene ut av komponentmappa, slik at ../shared.js blir @fristil/designsystem/shared. Oppslaget bygges av exports i pakken, så det som kopieres faktisk virker der det lander. En nabo i samme mappe blir med i kopien og står urørt.

Rekkefølgen teamet skal prøve: tokens først, så komponentvariabler, så egen CSS i sitt eget lag, og til sist overta. De tre første koster ingenting å oppgradere fra.

ProduktteameneStår aldri fast på grunn av oss.

DesignsystemteametFår se hva folk overtar, og det er den beste lista over hva som mangler.

Demoen som viser at det stemmer

Førstelinja er det samme spillet i tre utgaver: Kotlin med Datastar, React med TanStack Start, og Astro med hele sider fra serveren. Samme spilltjener, samme skjerm, samme komponenter, og du kan spille mot noen som sitter i en annen utgave.

Den er også en stresstest. En komponent kan se riktig ut i en enhetstest og likevel ryke i det en ekte server oppdaterer siden under den, og demoen er stedet der de to faktisk møtes.

Panelet i spillet: hva som faktisk gikk over nettverket, lest av trafikken

UtgaveOver nettverketHvor det rendres
DatastarSSE · datastar-patch-elements · HTML · 16 kBpå serveren
TanStack StartSSE · tilstand · JSON · 2,4 kBi nettleseren
AstroSSE · puls · JSON · 0,5 kB, og en ny side ved innsendingpå serveren

Tre helt ulike veier, og brukeren ser ingen forskjell. Spill den selv: forstelinja-react.up.railway.app, og trykk «Hva skjedde?» nede til høyre for å se trafikken mens du spiller.

ProduktteameneKan se sitt eget rammeverk i lista, og se at skjermen er den samme.

DesignsystemteametMå holde tre ekte apper i live ved siden av pakken. Enhetstester ser ikke det disse ser.

Hva vi sitter igjen med

Ett designsystem uten rammeverk i bunnen, som virker i det vi har i dag og i det vi bytter til.

For teamene

  • Én pakke, null avhengigheter, og ingen kode som må kjøre før en knapp ser riktig ut.
  • Samme komponenter i React, Astro, ren HTML og i en server som ikke kjører JavaScript.
  • Tilgjengeligheten er med fra start, ikke noe som legges på etterpå.
  • Egen CSS vinner, og kildekoden kan overtas den dagen det trengs.

For oss som forvalter det

  • Hver testkjøring i tre motorer, og en ny nettleserfunksjon må vente på den tregeste.
  • Demospillet i tre utgaver må holdes i drift. Slutter det å virke, slutter vi å se feilene enhetstestene ikke tar.
  • Et klassenavn kan ikke døpes om uten at det blir en brytende endring.
  • Hver ny regel må ha en vaktpost, ellers er den glemt om en måned.

Dokumentasjon: fristil.netlify.app · Pakke: @fristil/designsystem