Tabs
Komponenten tar piltastene, og kobler fanene til panelene når markupen kommer uten koblingen.
I en fanerad skal Tab hoppe forbi hele raden og inn i panelet, mens piltastene flytter mellom fanene. Et sett knapper uten den koblingen gir én tabbestopp per fane, og skjermleseren sier «knapp» der den skulle sagt «fane, 2 av 3, valgt».
Slik tar du den i bruk
Section titled “Slik tar du den i bruk”Søknaden ble sendt 4. mars 2026. Saksnummer 2026-0481.
Tre vedlegg er lastet opp. Siste kom 6. mars.
Ingen meldinger i denne saken ennå.
<!-- Stilarkene siden trenger. Uten byggesteg er det `<link>`, ikke `import`: en nettleser slår ikke opp et pakkenavn. --><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/src/tokens/tokens.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/src/components/ramme/tabs/tabs.css"><!-- Eller alt i én fil: https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/dist/fristil.css --><!-- Rollene, id-ene og koblingen setter komponenten. Vil du at en annen fane enn den første skal være valgt, sett `hidden` på de andre panelene. --><fs-tabs id="saksfaner"> <div class="fs-tabs__list" aria-label="Deler av saken"> <button>Søknaden</button> <button>Vedlegg</button> </div> <div class="fs-tabs__panel">Søknaden</div> <div class="fs-tabs__panel">Vedlegg</div></fs-tabs>
<!-- Én gang for hele appen, i sidemalen. Ikke én gang per komponent. --><script type="module"> import { defineFsTabs } from "https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/dist/components/ramme/tabs/fs-tabs.js"
defineFsTabs()</script>
<script type="module"> document.getElementById("saksfaner").addEventListener("tab-select", (evt) => { console.log("valgte fane", evt.detail.index) })</script>// src/fristil.d.ts, gir <fs-tabs> typer i JSXimport "@fristil/designsystem/react-jsx"import { fs } from "@fristil/designsystem/react"import { defineFsTabs } from "@fristil/designsystem/tabs"import "@fristil/designsystem/tokens.css"import "@fristil/designsystem/tabs.css"
defineFsTabs()
const TITLER = ["Søknaden", "Vedlegg", "Meldinger"]
export function Saksfaner() { const faner = fs.tabs({ id: "sak", count: TITLER.length, label: "Deler av saken" })
return ( <fs-tabs> <div {...faner.list}> {faner.tabs.map((fane, i) => ( <button key={fane.id} {...fane}>{TITLER[i]}</button> ))} </div> {faner.panels.map((panel, i) => ( <div key={panel.id} {...panel}>Innhold for {TITLER[i]}</div> ))} </fs-tabs> )}Importen fra @fristil/designsystem/react gir className og tabIndex i stedet for HTML-navnene, så React ikke advarer.
---import { fs } from "@fristil/designsystem"import "@fristil/designsystem/tokens.css"import "@fristil/designsystem/tabs.css"
const titler = ["Søknaden", "Vedlegg", "Meldinger"]const faner = fs.tabs({ id: "sak", count: titler.length, label: "Deler av saken" })
---
<fs-tabs> <div {...faner.list}> {faner.tabs.map((fane, i) => <button {...fane}>{titler[i]}</button>)} </div> {faner.panels.map((panel, i) => <div {...panel}>Innhold for {titler[i]}</div>)}</fs-tabs>
<script> import { defineFsTabs } from "@fristil/designsystem/tabs" defineFsTabs()</script><!-- Datastar selv. Uten den gjør `data-`-attributtene under ingenting. --><script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar.js"></script>
<!-- Stilarkene siden trenger. Uten byggesteg er det `<link>`, ikke `import`: en nettleser slår ikke opp et pakkenavn. --><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/src/tokens/tokens.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/src/components/ramme/tabs/tabs.css"><!-- Eller alt i én fil: https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/dist/fristil.css -->
<!-- Én gang for hele appen, i sidemalen. Ikke én gang per komponent. --><script type="module"> import { defineFsTabs } from "https://cdn.jsdelivr.net/npm/@fristil/designsystem@0.27.1/dist/components/ramme/tabs/fs-tabs.js"
defineFsTabs()</script>
<fs-tabs server-controlled data-on:tab-select="$valgtFane = evt.detail.index"> <div class="fs-tabs__list" aria-label="Deler av saken"> <button>Søknaden</button> <button>Vedlegg</button> </div> <div class="fs-tabs__panel">Søknaden</div> <div class="fs-tabs__panel" hidden>Vedlegg</div></fs-tabs>server-controlled hører med når et signal styrer hvilken fane som vises. Uten det kjemper de to om det samme: komponenten setter tilbake fanen brukeren valgte, mens serveren sender sin egen. Lar du signalet være, trengs ingenting, og komponenten holder valget selv.
Skriver serveren hidden på panelene, står det. Kommer markupen uten, viser tabs.css bare det første synlige panelet til komponenten er registrert, og komponenten skjuler resten i det den kobles til. Innholdet hopper derfor ikke i noen av tilfellene.
Hvilken fane som er valgt
Section titled “Hvilken fane som er valgt”selected sier hvilken fane som er valgt når markupen lages, og er indeksen i lista:
const faner = fs.tabs({ id: "sak", count: 3, selected: 1, label: "Deler av saken" })Serveren bestemmer altså utgangspunktet. Etterpå er det aria-selected i markupen som gjelder, og komponenten leser derfra. Mangler aria-selected, som i markup skrevet uten JavaScript, leser den valget fra panelene: det første som ikke har hidden hører til den valgte fanen. Sier markupen ingenting, eller skjuler den alle panelene, er det den første. Det komponenten selv har skrevet teller ikke som markupens svar: bytter serveren fane ved bare å flytte hidden, følger markeringen med.
server-controlled slår av at komponenten setter brukerens valg tilbake etter en patch. Den skriver fortsatt ut det markupen sier, altså aria-selected, tabbestoppet og hidden i takt, så en bar rad får rollene og valget også der.
Komponenten har ikke noe selected-attributt. Svaret står i markupen, som aria-selected på fanen, og leses derfra. To steder å holde i synk er ett for mye.
const faner = document.querySelector("fs-tabs")if (!faner) throw new Error("fant ikke elementet")
faner.selected // 0faner.select(1) // velger «Vedlegg» og sender `tab-select`faner.selected = 2 // det samme som select(2)Komponenten finner panelet til hver fane gjennom aria-controls. Panelene kan derfor stå i en annen rekkefølge enn fanene, og utenfor <fs-tabs>. Et panel utenfor ligger likevel ikke i det komponenten observerer, så river en patch hidden av det, kommer det ikke tilbake av seg selv. Peker aria-controls på en id som ikke finnes, sier komponenten fra i konsollen. Markup uten aria-controls kobles etter rekkefølgen, og komponenten skriver da koblingen inn, med id-er den lager selv og husker: en patch som river dem bort får de samme tilbake.
Før komponenten er registrert
Section titled “Før komponenten er registrert”Panelene står i siden før defineFsTabs() har kjørt. fs.tabs() skriver hidden på alle unntatt det valgte, så markupen er riktig fra første tegning.
Er markupen bar, viser fs-tabs:not(:defined) i tabs.css bare det første panelet som ikke er skjult. Regelen rekker bare panelene inne i <fs-tabs>: et panel utenfor må ha hidden fra serveren.
Klasser du kan style
Section titled “Klasser du kan style”| Klasse | Hva det er |
|---|---|
.fs-tabs__list | Raden med knapper. Får role="tablist" av serveren eller komponenten |
.fs-tabs__panel | Ett panel. Den valgte er uten hidden |
Knappene har ingen egen klasse, de styles som .fs-tabs__list button.
| Variabel | Reserve |
|---|---|
--fs-tabs-gap | var(--fs-spacing-1) |
--fs-tabs-padding | var(--fs-spacing-2) var(--fs-spacing-4) |
--fs-tabs-panel-space | var(--fs-spacing-4) |
.fs-tabs__list { --fs-tabs-gap: 0;}Tilgjengelighet
Section titled “Tilgjengelighet”Fanene er role="tab" i en role="tablist", og panelene er role="tabpanel" pekt til med aria-labelledby. Skriver serveren dem med fs.tabs(), står de i HTML-en før skriptet har kjørt. Kommer markupen uten, setter komponenten dem i det den registreres, og setter dem tilbake om en patch tar dem.
| Tast | Gjør |
|---|---|
| Piltastene | Flytter mellom fanene, og går rundt fra siste til første |
| Home og End | Første og siste fane |
| Tab | Ut av raden og inn i det valgte panelet |
En fane med disabled hoppes over, og i en side med dir="rtl" bytter venstre og høyre pil retning, slik at neste fane alltid er den som står ved siden av i leseretningen.
Én fane har tabindex="0", normalt den valgte. Er den valgte deaktivert, flyttes tabbestoppet til den neste aktive. Panelet har tabindex="0" slik at det får fokus også når det ikke har noe fokuserbart i seg, ellers hopper Tab rett forbi innholdet som nettopp ble vist.