@skagerakenergi/skagerak-design-components
v0.1.3
Published
Statkraft design tokens + BeerCSS theme bridge with Designsystemet coexistence via CSS layers
Readme
@skagerakenergi/skagerak-design-components
Delt temapakke som kobler Designsystemet og BeerCSS til én samlet, Skagerak-brandet visuell profil.
Innholdsfortegnelse
- Konsept og arkitektur
- Tilgjengelige temaer
- Hurtigstart
- Detaljert oppsett — steg for steg
- Mørk modus og temabytte under kjøring
- Enkel modus — ett tema, ingen bytte
- Eksponerte API-er
- Pakkestruktur
- CSS-lag og prioritetsrekkefølge
- Hvordan fargemapping fungerer
- Legge til et nytt tema
- Feilsøking
- Ekstern dokumentasjon
1. Konsept og arkitektur
Pakken løser ett konkret problem: BeerCSS og Designsystemet bruker begge globale CSS-variabler og stilregler som ellers ville overskrevet hverandre.
Løsningen er tredelt:
┌─────────────────────────────────────────────────┐
│ @layer beercss (lavest prioritet) │
│ BeerCSS – layout, spacing, Material Design 3 │
├─────────────────────────────────────────────────┤
│ @layer ds.* (mellomprioritet) │
│ Designsystemet – tokens, komponentstiler │
├─────────────────────────────────────────────────┤
│ (ikke-lagdelte regler) (høyest prioritet) │
│ App-spesifikke overstyringer │
└─────────────────────────────────────────────────┘Temabrobygging: Token-CSS-filene fra Designsystemet (f.eks. statkraft-blue.css) tolkes ved byggetid. Fargene leses ut og oversettes til BeerCSS sine egne CSS-variabler via ui('theme', …). Dermed styres begge rammeverk fra én enkelt kilde til sannhet.
Kjøretidsbytte: swapDsTokens() bytter ut <link>-elementet i <head> med en ny tokenfil. syncDsColorScheme() setter data-color-scheme-attributtet på <body>. BeerCSS sin ui('mode', …) og ui('theme', …) håndterer resten.
2. Tilgjengelige temaer
| ID | Label | Primærfarge |
| ------------------- | ----------------- | ----------- |
| statkraft-blue | Statkraft Blue | #00B0DC |
| statkraft-blue-15 | Statkraft Blue 15 | #DAF4FC |
| statkraft-blue-50 | Statkraft Blue 50 | #80DAF3 |
3. Hurtigstart
Forutsetter Vite + Vue 3 + pnpm. Tilpass etter behov for andre rammeverk.
1. Installer:
pnpm add @skagerakenergi/skagerak-design-components2. index.html:
<head>
<!-- Lag-deklarasjon — MÅ komme først -->
<style>
@layer beercss;
</style>
<!-- BeerCSS CSS via CDN, låst til laget -->
<style>
@import url('https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn/beer.min.css') layer(beercss);
</style>
<!-- BeerCSS JS + Material Dynamic Colors (kreves av ui('theme')) -->
<script
type="module"
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn/beer.min.js"
></script>
<script
type="module"
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn/material-dynamic-colors.min.js"
></script>
</head>3. main.ts:
import '@skagerakenergi/skagerak-design-components/styles'
import '@skagerakenergi/skagerak-design-components/web-components'4. App.vue (rotkomponent):
import {
defaultThemeVariant,
themeById,
themeVariants,
swapDsTokens,
syncDsColorScheme,
type ThemeVariantId
} from '@skagerakenergi/skagerak-design-components'
onMounted(async () => {
syncDsColorScheme('light')
swapDsTokens('statkraft-blue')
await ui('mode', 'light')
await ui('theme', themeById['statkraft-blue'])
})4. Detaljert oppsett — steg for steg
4.1 Installer pakken
# I roten av monorepoet (eller i app-mappen)
pnpm add @skagerakenergi/skagerak-design-componentsPakken er private: true og leveres kildekode-first (ingen separat byggsteg). Vite i konsumerende app kompilerer TypeScript direkte fra src/.
4.2 index.html — BeerCSS via CDN med lag-isolasjon
BeerCSS lastes ikke via npm-import — den lastes fra CDN direkte i HTML. Dette er nødvendig for at @layer-deklarasjonen skal fungere korrekt.
<!doctype html>
<html lang="no">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Min app</title>
<!--
Trinn 1: Deklarer lag-rekkefølgen.
Denne <style>-blokken MÅ stå FØR alle andre stilark.
Nettlesere bruker rekkefølgen her til å avgjøre prioritet
når det oppstår konflikter mellom lag.
-->
<style>
@layer beercss;
</style>
<!--
Trinn 2: Last inn BeerCSS CSS og plasser den i laget.
Nettlesere som ikke støtter @import layer() vil falle tilbake
til standard oppførsel (lite sannsynlig i 2026).
-->
<style>
@import url('https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn/beer.min.css')
layer(beercss);
</style>
<!--
Trinn 3: BeerCSS JavaScript — nødvendig for ui()-funksjonen.
Material Dynamic Colors — nødvendig for dynamisk tema-generering.
Begge lastes som ES-moduler for ikke å blokkere parsing.
-->
<script
type="module"
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn/beer.min.js"
></script>
<script
type="module"
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn/material-dynamic-colors.min.js"
></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>Viktig: Ikke importer
beercssviaimport 'beercss'i TypeScript/JavaScript — da mister du lag-isolasjonen og Designsystemet vil bli overstyrt.
4.3 main.ts — importer stiler og web-komponenter
// Designsystemet CSS (bruker @layer ds.* internt — vinner over @layer beercss)
import '@skagerakenergi/skagerak-design-components/styles'
// Registrerer alle Designsystemet web-komponenter (<ds-button>, <ds-field>, osv.)
import '@skagerakenergi/skagerak-design-components/web-components'
// Din app-kode som vanlig...
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// ...
app.mount('#app')Rekkefølge er viktig: Stilimporter må komme før app-montering for at CSS-lagene skal være registrert i korrekt rekkefølge.
4.4 TypeScript-typer
For at TypeScript skal kjenne til Designsystemet sine web-komponent-typer (f.eks. <ds-button> i Vue-templates), legg til i tsconfig.json eller tsconfig.app.json:
{
"compilerOptions": {
"types": ["@skagerakenergi/skagerak-design-components/globals"]
}
}Dette tilsvarer å referere til @digdir/designsystemet-web, @digdir/designsystemet-types og @digdir/designsystemet-css/theme direkte — pakket i ett sted.
4.5 App.vue / rotkomponent — tema-initialisering
Dette er det fullstendige mønsteret brukt i customerportal_beerless. Kopier og tilpass:
<script setup lang="ts">
import { ref, onMounted, watch, provide } from 'vue'
import {
defaultThemeVariant,
themeById,
themeVariants,
swapDsTokens,
syncDsColorScheme,
type ThemeVariantId
} from '@skagerakenergi/skagerak-design-components'
// BeerCSS sin ui()-funksjon er global (lastet via CDN-script i index.html)
declare global {
function ui(type: 'mode', value?: 'light' | 'dark' | 'auto'): Promise<string>
function ui(
type: 'theme',
value?: { light: string; dark: string }
): Promise<{ light: string; dark: string }>
}
const themeMode = ref<'light' | 'dark'>('light')
const themeVariant = ref<ThemeVariantId>(defaultThemeVariant)
// Gjør tilgjengelig for underkomponenter via provide/inject
provide('themeVariants', themeVariants)
provide('themeVariant', themeVariant)
provide('themeMode', themeMode)
onMounted(async () => {
// Les lagrede preferanser fra localStorage
const savedMode = localStorage.getItem('themeMode')
const savedVariant = localStorage.getItem('themeVariant') as ThemeVariantId | null
// Sett modus: lagret verdi → system-preferanse → 'light'
themeMode.value =
savedMode === 'light' || savedMode === 'dark'
? savedMode
: globalThis.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
// Sett variant: lagret verdi → standard
if (savedVariant && savedVariant in themeById) {
themeVariant.value = savedVariant
}
// Synkroniser begge rammeverk i parallell
syncDsColorScheme(themeMode.value) // setter data-color-scheme på <body>
swapDsTokens(themeVariant.value) // setter inn riktig token-CSS i <head>
await Promise.allSettled([
ui('mode', themeMode.value), // BeerCSS lys/mørk modus
ui('theme', themeById[themeVariant.value]) // BeerCSS fargepalett
])
})
// Reagér på modus-bytte (f.eks. fra en toggle-knapp)
watch(themeMode, (newMode) => {
syncDsColorScheme(newMode)
ui('mode', newMode)
localStorage.setItem('themeMode', newMode)
})
// Reagér på tema-bytte (f.eks. fra en tema-velger)
watch(themeVariant, (newVariant) => {
swapDsTokens(newVariant)
ui('theme', themeById[newVariant])
localStorage.setItem('themeVariant', newVariant)
})
</script>
<template>
<!-- body-elementet: BeerCSS bruker data-bs-theme, vi setter data-color-scheme via syncDsColorScheme -->
<RouterView />
</template>5. Mørk modus og temabytte under kjøring
Tre ting må skje synkront ved et modus- eller temabytte:
| Hva | Funksjon / metode | Påvirker |
| -------------------------- | ------------------------------------------ | ----------------------------------------- |
| Designsystemet fargetokens | syncDsColorScheme('dark' \| 'light') | data-color-scheme-attributt på <body> |
| Designsystemet tokenfil | swapDsTokens('statkraft-blue') | <link data-ds-tokens> i <head> |
| BeerCSS modus | ui('mode', 'dark') | BeerCSS CSS-variabler |
| BeerCSS fargepalett | ui('theme', themeById['statkraft-blue']) | BeerCSS farge-tokens |
Eksempel på en tema-toggle-knapp:
<script setup lang="ts">
import { inject, type Ref } from 'vue'
import {
themeById,
swapDsTokens,
syncDsColorScheme,
type ThemeVariantId
} from '@skagerakenergi/skagerak-design-components'
const themeMode = inject<Ref<'light' | 'dark'>>('themeMode')!
const themeVariant = inject<Ref<ThemeVariantId>>('themeVariant')!
function toggleMode() {
themeMode.value = themeMode.value === 'light' ? 'dark' : 'light'
// watch i App.vue tar seg av resten
}
</script>
<template>
<button @click="toggleMode">
{{ themeMode === 'dark' ? '☀️ Lys' : '🌙 Mørk' }}
</button>
</template>6. Bruksmønstre — hva trenger du?
Hva gjør ui() egentlig?
Det er viktig å skille mellom de to BeerCSS-kallene:
| Kall | Hva det gjør | Nødvendig for Statkraft-farger? |
| ------------------------------ | -------------------------------------------------------------------- | --------------------------------------------------------------------- |
| ui('mode', 'dark') | Setter class="dark" på <body> — BeerCSS sin egen lys/mørk-toggle | Nei — bare for BeerCSS sin mørk modus |
| ui('theme', { light, dark }) | Overskriver BeerCSS sine CSS-fargevariabler med Skagerak-tokens | Ja — uten dette bruker BeerCSS standard lilla Material You-palett |
ui('mode') og ui('theme') er helt uavhengige av hverandre. Du kan bruke én uten den andre.
Mønster A — Bare lys/mørk-bytte, ingen Skagerak-merkevare på BeerCSS
Bruk dette hvis du kun bruker BeerCSS til layout og ikke bryr deg om at BeerCSS-elementer får standard lilla farger.
// main.ts
import '@skagerakenergi/skagerak-design-components/styles'
import '@skagerakenergi/skagerak-design-components/tokens/statkraft-blue' // Designsystemet-tokens
import '@skagerakenergi/skagerak-design-components/web-components'<!-- App.vue -->
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { syncDsColorScheme } from '@skagerakenergi/skagerak-design-components'
const isDark = ref(false)
onMounted(() => {
isDark.value = matchMedia('(prefers-color-scheme: dark)').matches
syncDsColorScheme(isDark.value ? 'dark' : 'light')
// BeerCSS: ui('mode') setter bare class="dark" på <body> — det gjør vi manuelt:
document.body.classList.toggle('dark', isDark.value)
})
watch(isDark, (dark) => {
syncDsColorScheme(dark ? 'dark' : 'light')
document.body.classList.toggle('dark', dark)
localStorage.setItem('themeMode', dark ? 'dark' : 'light')
})
</script>Merk:
ui('mode', 'dark')er bare en snarvei fordocument.body.classList.add('dark'). Du kan kalleui('mode', …)i stedet om du foretrekker det — resultatet er identisk.
Mønster B — Fast Skagerak-tema + lys/mørk-bytte (anbefalt)
Ett fast tema, ingen variant-switching. BeerCSS får Skagerak-farger. Mørk modus fungerer for begge rammeverk.
// main.ts
import '@skagerakenergi/skagerak-design-components/styles'
import '@skagerakenergi/skagerak-design-components/web-components'
// Ikke importer tokenfil statisk her — swapDsTokens() setter den inn dynamisk<!-- App.vue -->
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { themeById, swapDsTokens, syncDsColorScheme } from '@skagerakenergi/skagerak-design-components'
const themeMode = ref<'light' | 'dark'>('light')
onMounted(async () => {
const saved = localStorage.getItem('themeMode')
themeMode.value =
saved === 'dark'
? 'dark'
: saved === 'light'
? 'light'
: matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
syncDsColorScheme(themeMode.value)
swapDsTokens('statkraft-blue')
await Promise.allSettled([
ui('mode', themeMode.value),
ui('theme', themeById['statkraft-blue']) // én gang — ikke byttet ut senere
])
})
watch(themeMode, (mode) => {
syncDsColorScheme(mode)
ui('mode', mode) // BeerCSS: kun class-toggle på <body>
localStorage.setItem('themeMode', mode)
// ui('theme') kalles IKKE her — fargepaletten er allerede satt og endres ikke
})
</script>Mønster C — Fullt tema- og mørk-modus-bytte
Se seksjon 4.5 for komplett eksempel med variant-switching og provide/inject.
7. Eksponerte API-er
Typer
type ThemeVariantId = 'statkraft-blue' | 'statkraft-blue-15' | 'statkraft-blue-50'
interface BeerTheme {
light: string // BeerCSS-temastreng for lys modus
dark: string // BeerCSS-temastreng for mørk modus
}
interface ThemeVariant {
id: ThemeVariantId
label: string // Visningsnavn, f.eks. "Statkraft Blue"
previewColor: string // Hex-farge for forhåndsvisning
theme: BeerTheme
}Konstanter
import {
themeVariants, // ThemeVariant[] — alle varianter, f.eks. for en dropdown
themeById, // Record<ThemeVariantId, BeerTheme> — rask oppslag
defaultThemeVariant // ThemeVariantId — 'statkraft-blue'
} from '@skagerakenergi/skagerak-design-components'Funksjoner
import { swapDsTokens, syncDsColorScheme } from '@skagerakenergi/skagerak-design-components'
// Bytter Designsystemet token-stilark i <head>
// Kall denne når brukeren velger et annet tema
swapDsTokens('statkraft-blue-50')
// Setter data-color-scheme="dark|light" på <body>
// Kall denne når brukeren bytter mellom lys og mørk modus
syncDsColorScheme('dark')CSS-eksporter (statisk import)
'@skagerakenergi/skagerak-design-components/styles' // Designsystemet CSS
'@skagerakenergi/skagerak-design-components/tokens/statkraft-blue' // Token-CSS: blå
'@skagerakenergi/skagerak-design-components/tokens/statkraft-blue-15' // Token-CSS: blå 15 %
'@skagerakenergi/skagerak-design-components/tokens/statkraft-blue-50' // Token-CSS: blå 50 %
'@skagerakenergi/skagerak-design-components/web-components' // Designsystemet web-komponenter8. Pakkestruktur
packages/skagerak-design-components/
├── package.json
├── tsconfig.json
└── src/
├── index.ts ← Hoved-API: typer, konstanter, funksjoner
├── styles.css ← Re-eksporterer @digdir/designsystemet-css
├── web-components.ts ← Registrerer alle ds-* web-komponenter
├── globals.d.ts ← TypeScript-referanser for ds-typer
├── env.d.ts ← Vite ?raw / ?url -støtte
├── head.html ← Kopi-paste-mal for index.html <head>
├── designsystemet.config.json ← Token-generatorkonfig (@digdir/designsystemet CLI)
├── design-tokens/ ← Kilde-tokens generert av Designsystemet CLI
└── design-tokens-build/ ← Bygde token-CSS-filer (script-generert, ikke rediger)
├── statkraft-blue.css ← Statkraft Blue (fullt tema)
├── statkraft-blue-15.css ← Statkraft Blue 15 %
├── statkraft-blue-50.css ← Statkraft Blue 50 %
├── colors.d.ts ← Fargetoken TypeScript-typer (deprecated, bruk types.d.ts)
└── types.d.ts ← CSS-modul TypeScript-typer9. CSS-lag og prioritetsrekkefølge
CSS Cascade Layers (@layer) er kjernen i isolasjonsstrategien. Nettleseren behandler lag i deklarert rekkefølge — lag deklarert sist vinner.
@layer beercss; ← deklarert i <style> i index.html (lavest)
@layer ds.theme.*; ← kommer fra statkraft-blue.css (vinner over beercss)
@layer ds.*; ← kommer fra @digdir/designsystemet-css (vinner over ds.theme.*)
(ingen lag) ← app-egne stiler vinner alltidFordi @layer beercss er deklarert uten innhold som den første @layer-setningen, vil alle andre lag (inkludert Designsystemets ds.*) automatisk ha høyere prioritet — uavhengig av rekkefølgen stilarkene lastes inn.
10. Hvordan fargemapping fungerer
Ved byggetid leser index.ts de bygde token-CSS-filene som rå tekst (?raw). CSS-parseren trekker ut alle --ds-*-variabler fra :root-blokken (lys modus) og [data-color-scheme="dark"]-blokken (mørk modus), og oversetter dem til BeerCSS-variabler via denne tabellen:
| BeerCSS-variabel | Designsystemet-kilde |
| --------------------- | ----------------------------------------- |
| --primary | --ds-color-accent-base-default |
| --on-primary | --ds-color-accent-base-contrast-default |
| --primary-container | --ds-color-accent-surface-tinted |
| --secondary | --ds-color-dark-blue-base-default |
| --background | --ds-color-neutral-background-default |
| --surface | --ds-color-neutral-surface-default |
| --outline | --ds-color-neutral-border-default |
| --error | --ds-color-orange-red-base-default |
| ... (se index.ts) | ... |
Resultatet er to strenger — én for lys og én for mørk modus — som sendes direkte til ui('theme', { light, dark }).
11. Legge til et nytt tema
Legg til tema i
src/designsystemet.config.jsonog kjør Designsystemets CLI for å generere tokenfiler isrc/design-tokens/. Deretter kjør build-skriptet som kompilerer tilsrc/design-tokens-build/mitt-nye-tema.css.Legg til import i
src/index.ts:import mittNyeTema from './design-tokens-build/mitt-nye-tema.css?raw'Utvid
ThemeVariantId:export type ThemeVariantId = | 'statkraft-blue' | 'statkraft-blue-15' | 'statkraft-blue-50' | 'mitt-nye-tema'Bygg tema og legg til i arrays:
const mittNyeTemaTheme = buildTheme(mittNyeTema) export const themeVariants: ThemeVariant[] = [ // ...eksisterende { id: 'mitt-nye-tema', label: 'Mitt Nye Tema', previewColor: '#AABBCC', theme: mittNyeTemaTheme } ] export const themeById: Record<ThemeVariantId, BeerTheme> = { // ...eksisterende 'mitt-nye-tema': mittNyeTemaTheme }Eksporter CSS-fil i
package.json:"./tokens/mitt-nye-tema": "./src/design-tokens-build/mitt-nye-tema.css"
12. Feilsøking
The stylesheet .../undefined was not loaded because its MIME type is "text/html"
swapDsTokens() fant ikke URL-en for varianten. Årsak: import.meta.glob-banen i index.ts samsvarer ikke med filplasseringen.
Sjekk: At alle .css-filer faktisk ligger i src/design-tokens-build/ og at glob-mønsteret er './design-tokens-build/statkraft-*.css'.
[postcss] ENOENT: no such file or directory, open 'beercss'
Du har en @import 'beercss' i en .css-fil. Fjern den — BeerCSS skal kun lastes via CDN <link> i index.html.
BeerCSS viser fremdeles standard lilla/blå farger
ui('theme', …) er ikke kalt, eller ble kalt før BeerCSS-scriptet var ferdig lastet fra CDN.
ui('theme', …)må kalles for at BeerCSS skal bruke Skagerak-farger. Uten det bruker BeerCSS sin innebygde lilla Material You-palett.- Kall
ui('theme', …)ionMounted()(ikke øverst i<script setup>). - Bruk
await Promise.allSettled([ui('mode', …), ui('theme', …)])for å vente på begge.
Designsystemet bytter ikke farge ved mørk/lys-toggle
To ting må skje:
syncDsColorScheme('dark')— setterdata-color-scheme-attributtet på<body>.- Den riktige tokenfilen må være lastet (gjøres av
swapDsTokens()).
Sjekk i devtools at <body data-color-scheme="dark"> er satt, og at <link data-ds-tokens="..."> finnes i <head>.
TypeScript klager på <ds-button> og andre web-komponenter
Legg til i tsconfig.json:
{
"compilerOptions": {
"types": ["@skagerakenergi/skagerak-design-components/globals"]
}
}ui is not defined
BeerCSS JavaScript-scriptet er ikke lastet. Kontroller at disse to linjene finnes i index.html før <script type="module" src="/src/main.ts">:
<script
type="module"
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn/beer.min.js"
></script>
<script
type="module"
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn/material-dynamic-colors.min.js"
></script>13. Ekstern dokumentasjon
| Ressurs | URL |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Designsystemet — oppsett og komponenter | https://designsystemet.no/no/fundamentals/code/setup |
| Designsystemet — fargesystem og tokens | https://designsystemet.no/no/fundamentals/design/colors |
| Designsystemet — GitHub | https://github.com/digdir/designsystemet |
| BeerCSS — hjemmeside og komponent-eksempler | https://www.beercss.com |
| BeerCSS — innstillinger / ui()-dokumentasjon | https://github.com/beercss/beercss/blob/main/docs/SETTINGS.md |
| BeerCSS — elementer | https://github.com/beercss/beercss/blob/main/docs/ELEMENTS.md |
| BeerCSS — hjelpere | https://github.com/beercss/beercss/blob/main/docs/HELPERS.md |
| BeerCSS — CDN (jsDelivr) | https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn/ |
| CSS Cascade Layers — MDN | https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers |
