@skagerakenergi/design
v0.39.0
Published
Skagerak Energi design system — M3 component library (CSS only)
Readme
@skagerakenergi/design
Material Design 3 (M3) component library for Skagerak Energi.
This package is CSS-first — most components are pure CSS + your own markup:
- M3 token files
- Per-component CSS files
- One convenience bundle (
index.css)
A few overlay components ship optional, tree-shakeable ESM helpers for behavior CSS cannot provide (each with a CSS-only fallback):
menu.js,dialog.js,tooltip.js— small JS enhancers (focus management, scroll lock, popover stacking)bottom-sheet.js— the self-registering<se-bottom-sheet>web component (drag/snap/swipe)
Install
pnpm add @skagerakenergi/designUpgrading
Unreleased (upgrading from 0.36.0 or earlier) — ⚠️ breaking typography change
The baseline Title Medium, Title Small, Label Large, Label Medium, and Label Small type styles were corrected from font-weight 700 to the M3-canonical font-weight 500. The previous 700 value was a bug — it is actually M3's emphasized weight for those roles, not the baseline.
This changes the rendered weight (bold → medium) of:
- Button and Extended FAB labels (
--md-sys-typescale-label-large-weight) - Chip labels, Menu items, List item titles (
--md-sys-typescale-label-large-weight/-medium-weight) - Badge text (
--md-sys-typescale-label-small-weight) - Navigation Bar / Navigation Rail labels (
--md-sys-typescale-label-medium-weight) - Tab labels, Extended FAB label fallback, Toolbar/App Bar titles using Title Medium/Small (
--md-sys-typescale-title-medium-weight/-small-weight)
No class names, markup, or CSS custom property names changed — only the computed weight value these tokens resolve to. If your product relied on the bold (700) appearance for these roles, override it explicitly:
:root {
--md-sys-typescale-label-large-weight: 700; /* restore old behavior if needed */
}See .specs/design-tokens.md for the full corrected type scale table and the planned emphasized type scale (a future opt-in 700-weight variant for these same roles).
Quick start (all styles)
Use index.css to load token files and all components.
@import '@skagerakenergi/design/index.css';<button class="se-button se-button--filled">Save</button>This is the fastest option when you want all components and automatic theme handling.
Recommended import (theme + selected components)
Load auto theme, system tokens, and only the components you use.
@import '@skagerakenergi/design/tokens/theme-auto.css';
@import '@skagerakenergi/design/tokens/system.css';
@import '@skagerakenergi/design/button.css';
@import '@skagerakenergi/design/icon-button.css';Use this mode to keep bundle size smaller by importing only the components you use.
Import only a single component
@import '@skagerakenergi/design/tokens/theme-auto.css';
@import '@skagerakenergi/design/tokens/system.css';
@import '@skagerakenergi/design/chips.css';<div class="se-chip-set" role="listbox" aria-label="Filter options">
<button class="se-chip se-chip--filter" aria-pressed="false">Option</button>
</div>Theming
The package supports six theme variants:
lightdarklight-medium-contrastdark-medium-contrastlight-high-contrastdark-high-contrast
Automatic theming (recommended)
theme-auto.css sets light tokens by default and automatically adapts to user OS settings for dark mode and high contrast.
@import '@skagerakenergi/design/tokens/theme-auto.css';
@import '@skagerakenergi/design/tokens/system.css';theme-auto.css includes:
- Light defaults on
:root - Automatic dark mode via
prefers-color-scheme: dark - Automatic high contrast via
prefers-contrast: more - Manual override classes (
.light,.dark,.light-medium-contrast,.dark-medium-contrast,.light-high-contrast,.dark-high-contrast)
Manual theme swapping
Import explicit scheme files and set a theme class yourself.
@import '@skagerakenergi/design/tokens/light.css';
@import '@skagerakenergi/design/tokens/dark.css';
@import '@skagerakenergi/design/tokens/light-mc.css';
@import '@skagerakenergi/design/tokens/dark-mc.css';
@import '@skagerakenergi/design/tokens/light-hc.css';
@import '@skagerakenergi/design/tokens/dark-hc.css';
@import '@skagerakenergi/design/tokens/system.css';
@import '@skagerakenergi/design/button.css';Set a theme class on any ancestor element (html, body, or a section wrapper):
<body class="dark">
<button class="se-button se-button--filled">Save</button>
</body>Swap class at runtime in your app to change theme.
Bring your own tokens (custom theme)
Skip library color theme files and provide your own --md-sys-color-* values.
/* Your app token file */
:root {
--md-sys-color-primary: #1976d2;
--md-sys-color-on-primary: #ffffff;
--md-sys-color-surface: #fdfcff;
--md-sys-color-on-surface: #1a1c1e;
/* Provide the full M3 system color set used by your components */
}
/* Library imports */
@import '@skagerakenergi/design/tokens/system.css';
@import '@skagerakenergi/design/button.css';Use this when you want your own brand palette while keeping the same component CSS API.
Bring your own tokens + auto-engine (OS preferences without custom media queries)
Use this repository's auto-engine to generate a theme-auto.css from your own Material Theme Builder exports, so you keep automatic dark/high-contrast behavior.
Important:
@skagerakenergi/design/tokens/theme-auto.cssalways points to the package's built-in token set.- To use your own token set, import your own generated
theme-auto.cssfrom your codebase.
Supported input filenames:
light.cssdark.csslight-mc.cssdark-mc.csslight-hc.cssdark-hc.css
Minimal setup (directory input):
pnpm build:theme-auto -- --source-dir ./brand-tokensThis generates tokens/theme-auto.css using your files when present and falls back to the library defaults for missing variants.
Strict mode (require all 6 custom variants):
pnpm build:theme-auto -- --source-dir ./brand-tokens --strictPer-file override example:
pnpm build:theme-auto -- --source-dir ./brand-tokens --dark ./brand/dark.css --out ./tokens/theme-auto.cssThen import as usual:
@import './tokens/theme-auto.css';
@import '@skagerakenergi/design/tokens/system.css';
@import '@skagerakenergi/design/button.css';In short:
- Import package
theme-auto.csswhen you want the package defaults. - Import local
theme-auto.csswhen you want your own upstream tokens.
Customizing components (shape, sizing, spacing)
Beyond color theming, every component exposes its own shape, sizing, and spacing tokens as CSS custom properties, named --se-{component}-{property} (e.g. --se-button-height, --se-card-radius, --se-divider-thickness). These are independent of the color-token system above — use them to tweak an individual component's dimensions or corners without forking its CSS or writing overly-specific selectors.
Each token is declared inline with a fallback to the exact Material Design 3 spec default, so leaving a token unset always renders the canonical M3 value:
/* Give every square button a custom corner radius */
.se-button--square {
--se-button-square-radius: 4px;
}
/* Make navigation rail wider app-wide */
:root {
--se-navigation-rail-width: 120px;
}
/* One-off override on a single element */
<div class="se-card" style="--se-card-radius: 24px; --se-card-padding: 24px">
...
</div>Guidelines:
- Tokens can be set on
:root(applies everywhere), on a component's class (applies to every instance of that component), or inline via thestyleattribute (applies to a single instance) — normal CSS cascade rules apply. - Only shape (radius/corner), sizing (height/width/icon size), and spacing (padding/gap/inset) are covered by this tier. Colors, typography, and motion are controlled by the
--md-sys-*system tokens described above. - Setting a token never changes any other property — overriding
--se-button-heightdoesn't affect padding or icon size, so you can mix and match freely. - Every token is also exposed as a live control in each component's Storybook story — open the component's story and use the Controls panel to experiment with values before writing CSS.
- The full, current token list for every component (names, defaults, descriptions) is documented in
INSTRUCTIONS.mdunder each component's "Customization tokens" section, and the overall token architecture is described in.specs/design-tokens.md.
Icons
Components that display icons expect Material Symbols Outlined to be loaded. The icon font is not bundled — load it yourself so you control which icons are included.
Google Fonts (recommended)
Add a <link> in your <head>. Use the &icon_names= parameter to include only the icons you need (keeps the download small):
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:[email protected]&icon_names=home,search,menu,close&display=block" rel="stylesheet" />[email protected]— enables the filled/outlined toggle used by navigation components&display=block— prevents a flash of unstyled ligature text&icon_names=— comma-separated list of the icons your app uses
Self-hosting
Download the WOFF2 from google/material-design-icons and add:
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
src: url('./material-symbols-outlined.woff2') format('woff2');
}
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
font-variation-settings: 'FILL' 0;
}Using icons in markup
<span class="material-symbols-outlined" aria-hidden="true">home</span>See .specs/icons.md for the full icon catalog and integration details.
Usage patterns
All components
@import '@skagerakenergi/design/index.css';Selected components
@import '@skagerakenergi/design/tokens/theme-auto.css';
@import '@skagerakenergi/design/tokens/system.css';
@import '@skagerakenergi/design/button.css';
@import '@skagerakenergi/design/card.css';
@import '@skagerakenergi/design/text-field.css';Manual theme + selected components
@import '@skagerakenergi/design/tokens/light.css';
@import '@skagerakenergi/design/tokens/dark.css';
@import '@skagerakenergi/design/tokens/system.css';
@import '@skagerakenergi/design/navigation-bar.css';
@import '@skagerakenergi/design/switch.css';Custom tokens + selected components
@import './my-theme.css'; /* provides --md-sys-color-* */
@import '@skagerakenergi/design/tokens/system.css';
@import '@skagerakenergi/design/text-field.css';Custom auto-theme + selected components (upstream token files)
@import './tokens/theme-auto.css'; /* generated from your own Theme Builder CSS files */
@import '@skagerakenergi/design/tokens/system.css';
@import '@skagerakenergi/design/text-field.css';Available imports
Token files:
@skagerakenergi/design/tokens/theme-auto.css@skagerakenergi/design/tokens/system.css@skagerakenergi/design/tokens/light.css@skagerakenergi/design/tokens/dark.css@skagerakenergi/design/tokens/light-mc.css@skagerakenergi/design/tokens/dark-mc.css@skagerakenergi/design/tokens/light-hc.css@skagerakenergi/design/tokens/dark-hc.css
Component files:
app-bar.css,badge.css,button.css,button-group.css,card.csscheckbox.css,chips.css,divider.css,extended-fab.css,fab.cssicon-button.css,list.css,loading-indicator.css,menu.css,navigation-bar.cssnavigation-rail.css,progress-indicator.css,radio.css,snackbar.csssplit-button.css,switch.css,tabs.css,text-field.css,toolbar.csstooltip.cssbottom-sheet.css— CSS-only bottom sheet styles (standalone<dialog>usage without web component)dialog.css— Dialog component styles (basic + full-screen variants)layout.css— spacing utilities, breakpoints, canonical layouts (list-detail, supporting pane, feed, split-pane), column grids (.se-columns-2/3/4), auto-fill grid (.se-grid), flex containers (.se-row,.se-column), flex/alignment utilities
Optional JavaScript modules (overlay components):
menu.js— keyboard navigation + focus management for Menu (ESM, TypeScript declarations atmenu.d.ts)tooltip.js—initTooltips()enablespopover="hint"stacking for fixed tooltips so they render above open popovers (ESM, TypeScript declarations attooltip.d.ts)dialog.js— open/close animation coordination, scroll lock, focus return for Dialog (ESM, TypeScript declarations atdialog.d.ts)
Web components (self-registering custom elements):
bottom-sheet.js—<se-bottom-sheet>modal bottom sheet with drag/snap/swipe (ESM, TypeScript declarations atbottom-sheet.d.ts)
Loading indicator shape asset (avoids inlining the large SVG path data):
loading-indicator-shapes.svg— import with?rawin Vite-based projectsloading-indicator-shapes.js— ESM export (loadingIndicatorShapesSvg: string) for non-Vite environments
Brand components (not part of M3 — use Skagerak brand palette, no M3 tokens required):
brand/logo.css— Skagerak group logo (sk-logo,sk-logo__mark,sk-logo__brand,sk-logo__subsidiary)brand/skagerak.svg— globe emblem used as thesk-logo__markimage in the font-based logo variants (<img src="skagerak.svg">)brand/skagerak_globe_white.svg— monochrome globe emblem (fill="currentColor") for inline embedding in the flat logo variant (sk-logo--flat)brand/skagerak_flat_colorpatch.svg— complete pre-rendered SVG lockup, globe + "SKAGERAK" wordmark (fill="currentColor") for thesk-logo--svgvariantbrand/skagerak_energitjenester_flat_colorpatch.svg— complete pre-rendered SVG lockup, globe + "Skagerak" + "Energitjenester" (fill="currentColor") for thesk-logo--svgvariantbrand/lede-logo.css— Lede group logo, a single-colour wordmark (lede-logo,lede-logo__img)brand/lede-wordmark.svg— monochrome Lede wordmark (fill="currentColor") for inline embedding + CSS colour controlbrand/lede-green-rgb.svg— original Lede logo, dark green#234639(unmodified, for<img>/ download use)brand/lede-white-rgb.svg— original Lede logo, white#ffffff(unmodified, for<img>/ download use)brand/lede-black-rgb.svg— original Lede logo, black#303030(unmodified, for<img>/ download use)
Figma Code Connect
This project uses Figma Code Connect to link Figma component instances to the corresponding HTML/CSS snippets so designers see real code in the Figma Dev Mode inspector.
Prerequisites
Figma access token — create one at Figma → Settings → Personal access tokens with the minimum scopes used by Code Connect publishing:
file_code_connect:write(Write and change component code)file_dev_resources:writefile_content:readfile_metadata:read
In the Figma token UI, these are shown under Development and Files with human-readable labels. Use the scope IDs above as the source of truth when documenting automation.
Export the token as
FIGMA_ACCESS_TOKEN:export FIGMA_ACCESS_TOKEN=figd_xxxxx@figma/code-connectis already a dev dependency (seepackage.json).
Project config
figma.config.json at the repo root tells the CLI where to find Code Connect files:
{
"codeConnect": {
"include": ["src/components/**/*.figma.ts"],
"exclude": ["node_modules/**", "dist/**", "storybook-static/**"],
"label": "HTML"
}
}include— glob for Code Connect source files.label— the language label shown in Figma Dev Mode (e.g."HTML").
File naming convention
Each component that has a Code Connect mapping gets a *.figma.ts file alongside its CSS and Storybook files:
src/components/button/
button.css
button.stories.ts
button.figma.ts ← Code Connect fileAnatomy of a Code Connect file
A Code Connect file imports the HTML helpers and calls figma.connect() once per Figma component set (node). Here is a minimal example:
import figma, { html } from '@figma/code-connect/html'
figma.connect(
// 1. Figma node URL — right-click a component set in Figma → "Copy link to selection"
'https://www.figma.com/design/<FILE_KEY>/<FILE_NAME>?node-id=<NODE_ID>',
{
// 2. Props — map Figma variant properties to code values
props: {
className: figma.className([
'se-button',
'se-button--filled',
figma.enum('Size', {
XSmall: 'se-button--xs',
Medium: 'se-button--md',
Large: 'se-button--lg',
XLarge: 'se-button--xl'
})
]),
label: figma.string('Label text'),
icon: figma.boolean('Show icon', {
true: html`<span class="se-button__icon material-symbols-outlined">add</span>`,
false: undefined
})
},
// 3. Example — the HTML snippet shown in Figma Dev Mode
example: ({ className, label, icon }) => html`\
<button class="${className}">
${icon}
${label}
</button>`,
// 4. Imports — CSS import hint displayed alongside the snippet
imports: ["@import '@skagerakenergi/design/button.css';"]
}
)Key prop helpers
| Helper | Purpose | Example |
|---|---|---|
| figma.className([...]) | Build a class string from static values and variant mappings | figma.className(['se-button', figma.enum('Style', { Filled: 'se-button--filled' })]) |
| figma.enum('Prop', map) | Map a Figma enum property to code values | figma.enum('Size', { Small: 'sm', Large: 'lg' }) |
| figma.string('Prop') | Pull a text/string property value straight through | figma.string('Label text') |
| figma.boolean('Prop', { true, false }) | Map a boolean toggle to different code outputs | figma.boolean('Show icon', { true: html\...`, false: undefined })|
|figma.instance('Prop')| Render a nested Figma instance with its own Code Connect |figma.instance('Leading icon')` |
Writing a new Code Connect file step by step
Identify the Figma component set. Open the Figma file, navigate to the component, right-click the top-level component set, and choose Copy link to selection. The URL contains the
node-idyou need.Create the file. Add
src/components/<name>/<name>.figma.ts.Map props. Open the component's property panel in Figma to see every variant property (e.g.
Size,Type,Style,Show icon). Map each one using the helpers above.Write the
examplefunction. Return anhtmltagged template that produces the same markup the component's CSS expects. Reference the mapped props by name.Add
imports. List the CSS@importstatement(s) a consumer needs.Handle multiple variants. If the Figma file splits a component into separate component sets (e.g. filled button vs outlined button), add a separate
figma.connect()call for each and hardcode the modifier class. See button.figma.ts for a real example.Validate locally.
# Parse all Code Connect files and report errors without publishing pnpm figma:publish --dry-runPublish to Figma.
pnpm figma:publishThis pushes the snippets to Figma so they appear in Dev Mode for the linked components.
Unpublish (remove all snippets).
pnpm figma:unpublish
Tips
- One
figma.connect()per component set node. If a component has five style variants as separate nodes in Figma, write five calls. - Prop names are case-sensitive and must match the Figma property name exactly (e.g.
'Label text', not'label text'). - Use
htmltagged templates (from@figma/code-connect/html) so Figma renders the snippet with proper syntax highlighting. - Backslash after the opening backtick (
html\\`) prevents a leading newline in the rendered snippet. importsis an array of strings — each string is shown as-is in the Figma UI. Use the package-level import path (@skagerakenergi/design/...) so consumers see the right import.- Keep snippets minimal — show the most common usage; don't try to cover every edge case.
- Refer to existing files for patterns: button.figma.ts, icon-button.figma.ts.
