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 krever det i dag et rammeverk du ikke kan bytte ut, og en omskriving hver gang rammeverket skifter retning.
Dette er syv krav et designsystem må oppfylle for å slippe det. For hvert av dem: hva det går ut på og hva det koster. Ingen av dem er gratis, og prisen står nederst på lysbildene.
Alt hviler på denne ene setningen:
Markupen eies av den som rendrer den. Oppførselen eies av komponenten.
Det gjennomgående eksempelet er Fristil, et designsystem bygget etter disse kravene og publisert på fristil.sobernetics.no og på npm som @fristil/designsystem.
Rammeverk kommer og går. Et designsystem som er bundet til ett av dem, må skrives om hver gang rammeverket skifter retning. Skal det stå igjen når appene rundt byttes ut, må det bygge på det nettleseren allerede forstår.
!important.Arbeidet forsvinner ikke. Det flyttes til dem som lager systemet: tester i tre nettlesermotorer, venting på den siste nettlesermotoren som mangler en funksjon, og klassenavn som ikke kan endres fordi de står i koden til andre.
Nederst på lysbildene står inntil tre linjer: hva kravet betyr for teamene som bruker systemet, hva det koster dem som forvalter det, og hvordan Fristil har løst det.
Resten av presentasjonen tar ett krav om gangen. Det første gjør de andre mulige, og det siste gjør at du kan forlate systemet når du vil. Til slutt kommer fellene, og så Fristil i bruk: et skjemafelt og et spill som viser at arkitekturen virker.
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.
node_modulesKrav 1 av 7 · Nettleseren er plattformen
Grunnen til ikke å bygge på React er ikke at noe annet er bedre i dag. Grunnen er at 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.
DesignsystemteametMå gjøre uavhengigheten usynlig i hverdagen, ellers oppleves den som et tilbakeskritt sammenlignet med et React-bibliotek.
I FristilInngangen @fristil/designsystem/react gir de samme byggefunksjonene med className og htmlFor.
Krav 1 av 7 · Nettleseren er plattformen
Det meste av det som er vanskelig i et designsystem, er allerede løst i nettleseren. Bygger systemet på nettleseren, er det kode ingen trenger å vedlikeholde.
showModal() gjør resten av siden utilgjengelig, holder fokus inne og lukkes med Escape.z-index.:invalid finnes og virker uten en eneste linje fra systemet.Det som gjenstår, er koblingen mellom delene, tastaturstyringen der nettleseren ikke har en ferdig kontroll, og et enhetlig utseende. Det er en langt mindre jobb enn å bygge fokus, lag og skjemaer selv.
Krav 1 av 7 · Nettleseren er plattformen
Uavhengigheten er ikke gratis. Den er betalt et annet sted: av dem som lager systemet, ikke av teamene som bruker det.
nettlesermotorer i hver testkjøring: Chromium, Firefox og WebKit. Hele premisset er at systemet bruker nettleserens egne API-er, og det er nettopp de som oppfører seg ulikt fra motor til motor.
Kravet er ikke at alle tre motorene har funksjonen. Kravet er at motoren som mangler den, gir noe som virker.
appearance: base-select Chromium 148 ✓ WebKit 26.4 ✓ Firefox 150 ✗
Derfor er den valgfri og ikke standard. Uten støtte får brukeren nettleserens egen nedtrekksliste.
Klassenavn, data-*, tokennavn og stiene i pakken er alt sammen noe en konsument bygger på. Et brudd på dem krever ny hovedversjon. Det komponenten gjør i nettleseren, er oppførsel og kan rettes som en vanlig feil.
DesignsystemteametMå ha en vaktpost for hver regel, ellers er den glemt om en måned.
I FristilRundt 1 400 tester kjører i hver av de tre motorene. Den stylede nedtrekkslista slås på med data-picker="styled".
Krav 1 av 7 · Nettleseren er plattformen
Et designsystem stiller krav til appen som tar det i bruk. Dette er kravene Fristil stiller, og til sammenligning kravene et React-basert designsystem stiller, hentet fra peerDependencies i @skatteetaten/ds-forms 3.0.0:
For å bruke skjemakomponentene i det systemet må appen din ha tolv pakker: React 19, i18next, react-i18next, date-fns og åtte pakker fra systemet selv. Fristil krever ingen, og hvilket rammeverk appen 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 dem som ikke bruker React.
Én knapp på skjermen, i en tom app. Bygget med esbuild, gzippet.
| Det som må lastes | Fristil | Et React-basert system |
|---|---|---|
| JavaScript | 0 kB, knappen er en klasse | 119,0 kB |
| CSS | 1,9 kB | 4,3 kB |
| Til sammen | 1,9 kB | 123,3 kB |
Av de 119 kilobytene er 67 React selv. Har du React fra før, legger det systemet til rundt 51 kB for den ene knappen. Byggefunksjonen fs med typer koster 3,9 kB i tillegg i Fristil.
Pakken har ingen avhengigheter, og ingenting i den gjør noe bare ved å bli importert. Hver modul kan lastes på en server uten DOM, så import { fs } virker med serverrendring.
Krav 1 av 7 · Nettleseren er plattformen
node_modulesFem kopier av en React- og TypeScript-app som har React 19 fra før. I hver av dem installeres det dokumentasjonen sier trengs for én knapp, og så leses package-lock.json. Lengst til høyre står pakkene alt annet hviler på.
Målt 26. september 2026, med versjonene som da var de nyeste. Aksel og Digdir har ingen egen knappepakke, så knappen koster hele React-pakken. Fristil er den samme pakken enten du bruker klassene eller byggefunksjonen: importen avgjør hva som lastes, ikke installasjonen. «CSS til knappen» er det som må lastes for én knapp når du bare bruker CSS (tokens og knapp), minifisert og gzippet som på forrige lysbilde. Stiplede linjer er peer-avhengigheter, som npm 7 og nyere henter selv.
Med CSR er det rammeverket i nettleseren, med SSR er det serveren, og ofte er det begge. Komponenten fester tastaturstyring, fokus og koblinger på markupen og legger aldri til noder eller attributter i markup andre har rendret. Av samme grunn bruker den ikke shadow DOM.
Krav 2 av 7 · Den som rendrer, eier markupen
Et designsystem må virke uansett hvor markupen rendres. Det finnes tre tilfeller, og de fleste team jobber med det første.
Rammeverket i nettleseren rendrer fra JSON. Endrer dataene seg, rendrer React treffene på nytt, og noder en web component har lagt inn, kan bli kastet.
const svar = await fetch("/api/treff")
setTreff(await svar.json())
Serveren rendrer HTML. Ved en oppdatering sender den HTML-en på nytt, og et bibliotek som Datastar morfer den inn. Attributter som ikke sto i det serveren sendte, blir fjernet.
Serveren rendrer først, React hydrerer, og så rendrer React i nettleseren. Markupen må være lik begge steder, ellers melder React avvik.
I alle tre rendrer noen andre enn komponenten DOM-en på nytt. Komponenten kan derfor ikke eie noe av markupen.
I Fristil<fs-suggestion> leser markeringen fra aria-selected og antall treff fra lista, så den virker også når React rendrer treffene på nytt etter et asynkront søk.
Krav 2 av 7 · Den som rendrer, eier markupen
Med SSR bygges HTML-en på serveren. Når siden oppdateres, sender serveren den biten av HTML-en på nytt, og et bibliotek fletter den inn i siden. Det kalles morfing, på engelsk DOM morphing. Nedenfor står 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.
rsNødutgangen er data-preserve-attr, som HTML-malen på serveren må fylle med navnene på alt komponenten har satt. Endres det komponenten setter, må hver mal endres i takt, uten at noen kompilator sier fra. Et designsystem kan derfor ikke bygge 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: rendres foreldrekomponenten på nytt, kan noder en web component har lagt inn, bli kastet bort. Derfor:
En komponent skal aldri legge til noder eller attributter i markup som noen andre rendrer.
Datastar er det strengeste av de tre, og det som virker der, virker også i TanStack Start og React Server Components. Kravet nevner likevel ikke noe rammeverk: det komponenten har satt, skal overleve at attributter blir fjernet.
Krav 2 av 7 · Den som rendrer, eier markupen
Regelen på forrige lysbilde deler systemet i to lag: det som rendres, og det komponenten gjør etterpå. Delingen er nødvendig fordi en ny rendring kan rive bort attributter og noder komponenten har lagt til, 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.
| Lag | Innhold | Hvem produserer det |
|---|---|---|
| Byggefunksjonene | Klasser, data-*, tilgjengelighetskoblingen | Den som rendrer: rammeverket i nettleseren eller en server skrevet i hvilket som helst språk |
| Web components | Tastatur, fokus, posisjonering | Komponenten i nettleseren, etter at markupen står der |
Ingen shadow DOM. Shadow DOM 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.
I FristilByggefunksjonene ligger i fs, for eksempel fs.button() og fs.field(), og alle web components heter <fs-…>.
Krav 2 av 7 · Den som rendrer, eier markupen
Med SSR skriver serveren all HTML, og med CSR gjør rammeverket det. Hvilken kategori en komponent hører til, handler derfor ikke om hvem som skriver HTML-en. Den handler om hvem som eier DOM-en mens siden lever.
Ingen eier noe. Markupen er statisk: en klasse og noen data-*.
<button class="fs-button" data-variant="secondary">
Den som rendrer, eier markupen. Komponenten lytter og rører verken noder eller attributter.
<fs-field> <label>…</label> <input class="fs-input"> </fs-field>
Meldinger, og linja som sier fra når forbindelsen ryker. De sender ingen verdi med skjemaet, og innholdet lager de selv når appen kaller dem. Har en komponent tekst som må oversettes, er den en ramme.
toast.show("Lagret")
Én setning avgjør hvor en komponent havner.
En komponent som sender en verdi med skjemaet eller har innhold som må komme fra den som rendrer, kan ikke eie sin egen markup. Den er en ramme.
Krav 2 av 7 · Den som rendrer, eier markupen
Det avgjørende er om koden som lager markupen, kan kalle en funksjon. Hvor den kjører, spiller ingen rolle.
FristilKoden 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 fra første rendring.
// Komponenten trengs ikke.
FristilMarkupen 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 et spørsmål om smak. 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 internt.
Krav 2 av 7 · Den som rendrer, eier markupen
Regelen sier at komponenten aldri legger til noe i markup andre har rendret. Likevel setter <fs-field> fra forrige lysbilde id, for og aria-describedby på slike elementer. Det henger sammen fordi alt komponenten skriver, er regnet ut fra det som ble rendret.
for, og id-er som ble rendret i aria-describedby, blir med i svaret.FristilHjelperen all skriving går gjennom, forenklet
function setAttr(el, navn, verdi) {
if (el.getAttribute(navn) !== verdi) el.setAttribute(navn, verdi)
}
ProduktteameneKan skrive så mye eller så lite av koblingen som de vil.
DesignsystemteametMå bygge komponenten slik at den holder rede på hva den selv har skrevet, node for node. Ellers kan den ikke skille sitt eget fra det som ble rendret.
I FristilMinnet er et WeakMap med noden som nøkkel. Fanene, dialogen og de andre er strengere enn feltet: der skriver komponenten bare attributter som mangler.
Reglene for hvordan et skjemafelt kobles sammen for en skjermleser, finnes ett sted i koden, og både byggefunksjonen og komponenten bruker den samme koden.
Krav 3 av 7 · Tilgjengeligheten skrives ett sted
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.
Fristilramme/field/field-core.ts
computeFieldAttributes(...) ├── fs.field() ← kalles der markupen rendres └── <fs-field> ← komponenten kaller den på elementer som står i DOM-en
Endres kontrakten, endres den der, ikke i komponenten. Det svarer også på spørsmålet som alltid kommer: Skal ikke hver server skrive dette selv? Den dagen hver server må gjøre det selv, er det et designsystem bare for JavaScript, med en manuell reserve for alle andre, og det er reserven som ryker først.
ProduktteameneFår den samme koblingen enten markupen lages i React, i en Go-mal eller for hånd.
DesignsystemteametMå skrive kontrakten som en ren funksjon uten DOM, slik at både koden som rendrer, og en komponent kan kalle den.
Finnes tilstanden i markupen, leser komponenten den derfra. Den holder aldri en egen kopi av noe som også rendres, og den setter tilbake det en ny rendring river bort.
Krav 4 av 7 · DOM-en er fasiten
En ny rendring kan fjerne det komponenten satte: morfingen stryker attributtet, og React kan bytte ut noden. Komponenten vet hva den selv har regnet ut og skriver det inn igjen med én gang. Den som rendrer, trenger derfor ikke vite noe om hva komponenten gjør:
FristilMarkupen, rendret med CSR eller SSR
<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.
FristilSkal den som rendrer, eie tilstanden alene
<fs-field server-controlled>
Ett attributt på verten, og komponenten reparerer ingenting. Da gjelder det som ble rendret, hver gang. Navnet sier server, men attributtet gjelder like mye for CSR.
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. Morfingen fjerner attributtet, observatøren setter det tilbake i samme mikrotask, og nettleseren rekker ikke å tegne imellom. Den rendrede utgaven står i DOM-en et øyeblikk uten at noen ser den.
DesignsystemteametMå sammenligne før hver skriving. Skriver komponenten den samme verdien på nytt, utløser observatøren seg selv, og testkjøringen henger i stedet for å feile.
Krav 4 av 7 · DOM-en er fasiten
Den som rendrer, og komponenten ser på den samme siden. Holder komponenten sin egen utgave av noe som også rendres, finnes tilstanden to steder, og da er det bare et spørsmål om tid før de to blir uenige.
At et felt er ugyldig, står som aria-invalid på kontrollen. Leser komponenten det derfra, er den alltid enig med rendringen. Leser den i stedet sitt eget attributt på verten, vil den før eller siden fjerne noe som nettopp ble rendret. I React viser det seg 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 som også rendres.
Hvilken fane som er valgt, leses derfor fra aria-selected og ikke fra et eget attributt. Når komponenten både leser og skriver et attributt, må den også 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 hvilke tilstander verten har hatt før.
Krav 4 av 7 · DOM-en er fasiten
HTML har ingen kompilator. Skriver et team et felt uten ledetekst, er det ingenting som stopper det, og siden ser helt riktig ut.
Den som merker feilen, er den som ikke ser skjermen: feltet blir lest opp uten navn. En komponent som gir opp uten et ord, sender feilen videre til brukeren. Den skal i stedet skrive én linje i konsollen, mens utvikleren fortsatt sitter med koden.
FristilMarkupen som ble skrevet
<fs-field> <input class="fs-input" id="fodselsdato"> </fs-field>
FristilDet 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 slår falsk alarm, blir ignorert innen en uke. Tre valg gjør at denne er til å stole på:
I FristilEn sjekk i CI leser konsollen på hver side i den bygde dokumentasjonen og feiler hvis det står en advarsel der.
Du skal ikke miste autofullføring og røde streker i editoren fordi du ikke bruker React. Byggefunksjonene er vanlige TypeScript-funksjoner som returnerer attributter.
Det er dette som gjør at rammeverksuavhengighet ikke oppleves som et tilbakeskritt.
Krav 5 av 7 · Typene virker uten rammeverk
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.
FristilKallet, 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
variant er visuell vekt, color er hva en status betyr, state er validering.Skriver du feil, for eksempel variant: "secondry", 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 kan du sjekke verdien ved kjøring med fs.button.isVariant().
ProduktteameneFår typene uten å installere et rammeverk, og uten at noen har skrevet en komponent for akkurat deres rammeverk.
DesignsystemteametMå holde de lovlige verdiene, typene og CSS-en i takt. En verdi som ikke gjør noe, er verre enn ingen verdi.
Krav 5 av 7 · Typene virker uten rammeverk
En kodeagent fyller hullene med det den har sett i andre designsystemer: fs-modal, data-variant="outline". Den stopper bare hvis noe sier fra mens den jobber.
FristilMed JavaScript sier TypeScript fra
// knapp.ts
fs.button({ variant: "outline" })
$ tsc
knapp.ts(2,34): error TS2322: Type '"outline"'
is not assignable to type '"danger" | "primary"
| "secondary" | "ghost" | undefined'.
Regelboka ber agenten bruke byggefunksjonene. En klasse skrevet for hånd i en streng ser TypeScript ikke.
FristilUten JavaScript leverer designsystemet sjekken selv
$ npx @fristil/designsystem sjekk side.html side.html:1:14: advarsel: Klassen «fs-modal» finnes ikke i Fristil. side.html:2:29: advarsel: data-variant kan ikke være «outline» på button. Lovlige verdier: secondary, ghost, danger, og primary uten attributt. 2 funn i 1 fil. $ echo $? 1
Sjekken leser HTML-en eller malen og avslutter med feilkode, slik tsc gjør. Uten funn svarer den «Markupen stemmer med Fristil i 1 fil.» og avslutter med 0.
DesignsystemteametMå generere regelbøkene fra koden og teste dem i hvert bygg. Nevner en bok en klasse, et element eller et token som ikke finnes, stopper bygget.
I FristilIndeksen over regelbøkene, én per miljø, står på fristil.sobernetics.no/llms.txt.
All komponent-CSS ligger i et eget lag. En helt vanlig regel skrevet av teamet slår systemets, uten !important og uten spesifisitetstriks.
Krav 6 av 7 · Konsumentens CSS vinner
Et team som må skrive !important for å flytte en knapp fire piksler, slutter å bruke systemet. Derfor ligger all komponent-CSS i et eget lag, og en regel uten lag slår alt som står i ett.
FristilUten laget taper regelen din på spesifisitet
.fs-button (0,1,0) .fs-button:disabled (0,2,0) ← systemets vinner
Med laget vinner regelen din uansett
@layer fristil {
.fs-button:disabled { … }
}
.fs-button { … } ← usortert, vinner
Form og størrelse leses fra en variabel
padding: var(--fs-button-padding,
var(--fs-spacing-2) var(--fs-spacing-4));
Standardverdien følger avstandsskalaen, også når konsumenten endrer --fs-spacing-*. Variablene er med vilje ikke registrert med @property: en registrert startverdi blir verdien når variabelen ikke er satt, og da kommer var() aldri fram til reserven.
ProduktteameneKan overstyre alt fra sin egen CSS, uten å kopiere systemets kode.
DesignsystemteametMå teste laget og variablene i en nettleser, ikke anta at de virker. Begge deler har egne tester.
Et designsystem dekker aldri alt. I stedet for å skrive et omslag rundt noe som ikke strekker til, skal teamet kunne kopiere kildekoden til én komponent inn i prosjektet sitt.
Krav 7 av 7 · Teamet kan ta over koden
Et team som trenger noe systemet ikke har tenkt på, skal komme videre samme dag. Alternativet er at de kopierer komponenten inn i prosjektet for hånd. Da mister teamet og systemet kontakten med hverandre, og feilrettinger når ikke fram.
Rekkefølgen teamet skal prøve: først tokens, så komponentvariabler, så egen CSS i et eget lag. Først når ingen av dem strekker til, tar teamet over komponenten. De tre første koster ingenting å oppgradere fra.
Det som gjør kopien brukbar, er at henvisningene ut av komponenten peker på pakken etterpå, og ikke på filer som ikke finnes hos teamet.
FristilKildekoden til én komponent, inn i prosjektet ditt
npx @fristil/designsystem overta button --ut=src/ui
Kommandoen skriver om henvisningene, slik at ../shared.js blir @fristil/designsystem/shared. Oppslaget bygges av exports i pakken, slik at det som kopieres, faktisk virker der det lander.
ProduktteameneStår aldri fast på grunn av systemet.
DesignsystemteametFår se hva folk overtar, og det er den beste lista over hva som mangler.
Kravene er enkle å beskrive. Feilene kommer der systemet møter React, en server uten DOM eller koden som kjører før komponenten er registrert. De fleste ble funnet i ekte apper, ikke i enhetstestene.
Fellene
Har en web component en egenskap med samme navn som et attributt, setter React 19 egenskapen og ikke attributtet. Det gir tre feil, og de to første gir ingen feilmelding:
el.open = "" er usant, og setteren fjerner attributtet igjen. Send true.autofocus="false" blir el.autofocus = "false", og en streng med innhold er sann. Legg valget i data-*, som React alltid sender som attributt.Stavemåten er også ulik. React vil ha className, htmlFor og tabIndex, og tabIndex som tall. En byggefunksjon som returnerer HTML-navn, trenger derfor en egen utgave for React.
DesignsystemteametMå teste i en ekte React-app. Feilene viste seg der, og ikke i enhetstestene.
I FristilFlaggene sendes som true, og feiloppsummeringens valg heter data-autofocus.
Fellene
Serveren og nettleseren lager hver sin id, React melder avvik, og koblingen mellom ledetekst og felt er brutt til React har rettet den opp. Gjør id påkrevd i typen, og legg en melding i konsollen ved siden av for dem typen ikke når.
HTMLElementclass X extends HTMLElement evalueres i det modulen lastes, og HTMLElement finnes bare i nettleseren. Da kan ikke en server importere noe fra pakken, heller ikke en byggefunksjon. Arv fra en klasse som faller tilbake på en tom klasse uten DOM.
Ingen nettlesertest fanger den siste, for der finnes HTMLElement. Den må testes i et miljø uten DOM.
En sjekk som må slutte fra miljøet til hva utvikleren mente, gjetter feil i minst ett miljø. Lukk fella i typen framfor å gjette.
I FristilHostElement er HTMLElement i nettleseren og en tom klasse uten DOM, og et skript importerer hver oppføring i exports i Bun uten DOM.
Fellene
Fram til komponenten er registrert, er hver web component et vanlig HTMLElement, uten metoder og uten oppførsel.
useEffect kjører etter første tegning. Kaller appen en metode på elementet før det, får den «reportSuccess is not a function».customElements.whenDefined() for elementets eget tagnavn, gjør rekkefølgen likegyldig. Den sier fra i konsollen hvis det tar for lang tid.:not(:defined) gjelder til komponenten er registrert. Med den kan stilarket vise bare det første panelet, så den som rendrer, slipper å skrive hidden på resten.I FristildefineFs() registrerer alle ni, og showToast, reportFailure og de andre imperative kallene venter selv.
Slik ser det ut for den som bruker et designsystem bygget etter de syv kravene. Eksempelet er Fristil. Du installerer én pakke, importerer stilarket for det du trenger og skriver vanlig HTML.
FristilBare 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>
FristilSamme 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 opp både ledeteksten og feilmeldingen.
ProduktteameneKan begynne med tokens og noen knapper og ta i bruk resten når de trenger det.
DesignsystemteametMå bevare denne enkelheten, også når en komponent blir komplisert på innsiden.
Førstelinja er et spill bygget med Fristil, 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 spiller 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 eller React rendrer siden på nytt. TanStack Start-utgaven rendrer i nettleseren fra JSON, slik en vanlig CSR-app gjør, og de to andre rendrer på serveren.
Panelet i spillet: hva som faktisk gikk over nettverket, lest ut av trafikken
| Utgave | Over nettverket | Hvor det rendres |
|---|---|---|
| Datastar | SSE · datastar-patch-elements · HTML · 16 kB | på serveren |
| TanStack Start | SSE · tilstand · JSON · 2,4 kB | i nettleseren |
| Astro | SSE · puls · JSON · 0,5 kB, og en ny side ved innsending | på 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.
Ett spørsmål per krav, og ett per felle. Kan du svare ja på alle, har systemet det som skal til.
!important?Fristil: fristil.sobernetics.no · Pakke: @fristil/designsystem · Spillet: forstelinja-react.up.railway.app