Card with the link in its title
The whole card clicks, but the link is named by its title and the rest reads as text.
Auto follows your device, and it allows motion.
Living brand book
This is not a PDF. Every colour, space and movement on this page is read from @hjconsult/tokens, the same tokens the site is built from. Switch the theme and the book switches with it.
Principles
Voice, motion and accessibility are not decoration added at the end. They live in the tokens, in the components and in the way we write.
Trygg, varm og rett på sak, med et glimt i øyet. Vi skriver «vi bygger», ikke «det leveres». Korte setninger og ingen buzzord. Tankestrek bare når pausen er selve poenget, og aldri punktum i en overskrift. Humor bare når setningen fortsatt gir mening uten den.
Written natively, never translated word for word. Confident, warm and plain-spoken. Contractions are fine, jargon is not. No dash as a pause and no full stop on a heading: a comma, a colon or a new sentence does the job better. The joke is allowed only if the sentence still works without it.
Everything exists in the HTML before JavaScript runs. Motion adds, and never reveals something that would otherwise be hidden.
Durations from motion.duration.*, curves from motion.ease.*, springs from motion.spring.*. No magic milliseconds in components.
prefers-reduced-motion zeroes every duration and drops parallax and shader animation. Everything has a static version.
Elements arrive fast and land softly (out-expo). Movement uses the standard curve. Loops use in-out.
Text on canvas ≥ 7:1 and muted text ≥ 4.5:1, tested for every theme and colour scheme at build time.
color.focus with a thick ring and a 2 px offset. We never remove outlines.
Landmarks, one h1 per page, real buttons and links. ARIA fills gaps and is never a patch.
Everything is reachable with Tab. Decorative effects are aria-hidden, and animated text keeps the real string in the DOM.
Logo
The logo comes as a horizontal lockup (mark + wordmark) and as a standalone mark. Both are vector and inherit the text colour, so they work on every surface in the system.
Keep at least the height of the mark free on every side. Nothing enters that zone: no text, icons or edges.
Horizontal lockup: never below 24 px tall on screen. Mark alone: never below 16 px.
SVG, PNG and favicon in both lockups, for print, decks and partners.
The pack lands here once the final files are ready. Need the logo now? Send us an email.
Request the logo pack by email/brand/hjconsult-logo-pack.zipColour
Components only use semantic tokens (color.*). The palettes behind them are OKLCH ramps, and each theme points the tokens at its own stops. The contrast below is computed at build time against color.canvas in each theme’s native scheme.
| Token | Use | Northern lights | Systems |
|---|---|---|---|
color.canvas--hj-color-canvas | Page background: the lowest layer everything sits on. | – | – |
color.surface--hj-color-surface | Default panel and card background, one step above canvas. | 1.1:1Decorative only | 1.0:1Decorative only |
color.surface-raised--hj-color-surface-raised | Elevated surface: popovers, sticky bars, hovered cards. | 1.1:1Decorative only | 1.1:1Decorative only |
| Token | Use | Northern lights | Systems |
|---|---|---|---|
color.fg--hj-color-fg | Primary text and icons. ≥ 7:1 on canvas in every theme and scheme. | 17.4:1AAA | 18.1:1AAA |
color.fg-muted--hj-color-fg-muted | Secondary text: metadata, descriptions. ≥ 4.5:1 on canvas. | 7.9:1AAA | 8.1:1AAA |
color.fg-subtle--hj-color-fg-subtle | Tertiary text for placeholders, disabled and decorative content. It only clears 3:1, so keep it to large or non-essential text. | 5.3:1AA | 5.2:1AA |
color.fg-on-accent--hj-color-fg-on-accent | Text and icons placed on accent fills. ≥ 4.5:1 on accent.vs color.accent | 14.4:1AAA | 13.2:1AAA |
color.accent-alt-fg--hj-color-accent-alt-fg | Text and icons placed on accent-alt fills.vs color.accent-alt | 7.0:1AAA | 14.8:1AAA |
| Token | Use | Northern lights | Systems |
|---|---|---|---|
color.accent--hj-color-accent | Primary brand accent: links, primary buttons, highlights. | 14.4:1AAA | 13.2:1AAA |
color.accent-strong--hj-color-accent-strong | Deeper accent for hover/active states and thin strokes that need ≥ 3:1 against canvas. | 10.5:1AAA | 10.1:1AAA |
color.accent-soft--hj-color-accent-soft | Accent at ~15 % alpha: selected rows, soft badges, tints. | 1.4:1Decorative only | 1.3:1Decorative only |
color.accent-alt--hj-color-accent-alt | Secondary accent: data visualisation, tags, the second hero colour. | 7.0:1AAA | 14.8:1AAA |
color.focus--hj-color-focus | Focus ring colour. Pair with border.width.thick and a 2px offset. | 14.4:1AAA | 13.2:1AAA |
| Token | Use | Northern lights | Systems |
|---|---|---|---|
color.line--hj-color-line | Hairline dividers and default borders (alpha, works over any surface). | 1.2:1Decorative only | 1.2:1Decorative only |
color.line-strong--hj-color-line-strong | Emphasised borders: inputs, table headers, hard rules. | 1.5:1Decorative only | 1.8:1Decorative only |
color.glass--hj-color-glass | Translucent fill for glass panels. Pair it with backdrop-filter. | 1.1:1Decorative only | 1.1:1Decorative only |
color.glass-line--hj-color-glass-line | Border for glass panels. | 1.4:1Decorative only | 1.5:1Decorative only |
| Token | Use | Northern lights | Systems |
|---|---|---|---|
color.positive--hj-color-positive | Success and confirmation: text-safe on canvas. | 9.1:1AAA | 9.3:1AAA |
color.warning--hj-color-warning | Caution and pending: text-safe on canvas. | 10.3:1AAA | 10.6:1AAA |
color.danger--hj-color-danger | Errors and destructive actions: text-safe on canvas. | 6.7:1AA | 6.9:1AA |
| Token | Use | Northern lights | Systems |
|---|---|---|---|
color.hero.a--hj-color-hero-a | Shader / illustration palette stop A: the base (darkest or lightest field). | 1.0:1Decorative only | 1.2:1Decorative only |
color.hero.b--hj-color-hero-b | Shader / illustration palette stop B: the mid-tone wash. | 4.0:1Large text | 7.3:1AAA |
color.hero.c--hj-color-hero-c | Shader / illustration palette stop C: the accent glow. | 14.4:1AAA | 14.9:1AAA |
color.hero.d--hj-color-hero-d | Shader / illustration palette stop D: the contrasting flare. | 4.7:1AA | 14.8:1AAA |
Contrast is computed on the sRGB fallback with alpha composited over canvas, the way a browser does it. The swatches themselves use var(--hj-…) and follow the theme you are looking at.
Typography
Display and body are set in Schibsted Grotesk, code and labels in Geist Mono. The scale is fluid: every step has a min and a max between breakpoint.sm and breakpoint.xl, with line-height and tracking baked into the Tailwind class.
var(--font-schibsted), "Schibsted Grotesk", ui-sans-serif, system-ui, sans-serifSphinx of black quartz, judge my vow.
Æ Ø Å æ ø å 0123456789 “quote” → ← ↑ ↓ 12 °C
var(--font-schibsted), "Schibsted Grotesk", ui-sans-serif, system-ui, sans-serifBlåbærsyltetøy, ærlig øl og sære måker på en rå fjordbrygge.
Sphinx of black quartz, judge my vow. Æ Ø Å æ ø å 0123456789 “quote” → ← ↑ ↓ 12 °C
Æ Ø Å æ ø å 0123456789 “quote” → ← ↑ ↓ 12 °C
var(--font-geist-mono), "Geist Mono", ui-monospace, "SFMono-Regular", monospaceÆ Ø Å æ ø å 0123456789 “quote” → ← ↑ ↓ 12 °C
:root {
--hj-color-accent: oklch(0.877 0.15 167);
--hj-motion-ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
}| Role | Specimen | Min | Max | Line-height | Tracking | Tailwind |
|---|---|---|---|---|---|---|
display-1 | Aa Ææ Øø Åå | 3rem | 8rem | 1.05 | -0.03em | text-display-1 |
display-2 | Aa Ææ Øø Åå | 2.5rem | 5.5rem | 1.05 | -0.03em | text-display-2 |
h1 | Aa Ææ Øø Åå | 2.25rem | 4rem | 1.05 | -0.015em | text-h1 |
h2 | Aa Ææ Øø Åå | 1.75rem | 2.75rem | 1.15 | -0.015em | text-h2 |
h3 | Aa Ææ Øø Åå | 1.375rem | 1.875rem | 1.15 | -0.015em | text-h3 |
h4 | Aa Ææ Øø Åå | 1.125rem | 1.375rem | 1.3 | 0 | text-h4 |
h5 | Aa Ææ Øø Åå | 1rem | 1.125rem | 1.3 | 0 | text-h5 |
h6 | Aa Ææ Øø Åå | 0.875rem | 1rem | 1.3 | 0.08em | text-h6 |
lead | Aa Ææ Øø Åå | 1.125rem | 1.375rem | 1.65 | 0 | text-lead |
body | Aa Ææ Øø Åå | 1rem | 1.125rem | 1.5 | 0 | text-body |
small | Aa Ææ Øø Åå | 0.875rem | 0.9375rem | 1.5 | 0 | text-small |
caption | Aa Ææ Øø Åå | 0.75rem | 0.8125rem | 1.5 | 0 | text-caption |
Tokens
Everything below is read from the token package at build time. The values are the real ones; the visualisations use var(--hj-…) and therefore follow the theme.
A 4 px grid. Tailwind classes mirror the multiplier: p-4 = space.4 = 1 rem. The fluid steps (gutter, section-*) are clamp() between sm and xl.
| Token | Value | Space | Tailwind |
|---|---|---|---|
space.px--hj-space-px | 1px | p-px | |
space.0-5--hj-space-0-5 | 0.125rem | p-0.5 | |
space.1--hj-space-1 | 0.25rem | p-1 | |
space.2--hj-space-2 | 0.5rem | p-2 | |
space.3--hj-space-3 | 0.75rem | p-3 | |
space.4--hj-space-4 | 1rem | p-4 | |
space.5--hj-space-5 | 1.25rem | p-5 | |
space.6--hj-space-6 | 1.5rem | p-6 | |
space.8--hj-space-8 | 2rem | p-8 | |
space.10--hj-space-10 | 2.5rem | p-10 | |
space.12--hj-space-12 | 3rem | p-12 | |
space.16--hj-space-16 | 4rem | p-16 | |
space.20--hj-space-20 | 5rem | p-20 | |
space.24--hj-space-24 | 6rem | p-24 | |
space.32--hj-space-32 | 8rem | p-32 | |
space.40--hj-space-40 | 10rem | p-40 | |
space.48--hj-space-48 | 12rem | p-48 | |
space.64--hj-space-64 | 16rem | p-64 | |
space.fluid.gutter.*--hj-space-gutter | 1rem → 2.5rem | Page gutter at the sm breakpoint (16px). | px-gutter |
space.fluid.section.sm.*--hj-space-section-sm | 2.25rem → 5rem | Compact section padding on a phone (36px). | py-section-sm |
space.fluid.section.md.*--hj-space-section-md | 3.25rem → 8rem | Default section padding on a phone (52px). | py-section-md |
space.fluid.section.lg.*--hj-space-section-lg | 4.5rem → 12rem | Hero/feature section padding on a phone (72px). | py-section-lg |
From xs for chips to 3xl for large panels. full gives pills and circles.
radius.xs2pxrounded-xsradius.sm4pxrounded-smradius.md6pxrounded-mdradius.lg8pxrounded-lgradius.xl12pxrounded-xlradius.2xl16pxrounded-2xlradius.3xl24pxrounded-3xlradius.full9999pxrounded-fullThree levels plus glow. The glow colour follows the theme accent.
shadow.1shadow-1shadow.2shadow-2shadow.3shadow-3shadow.glowshadow-glowDurations, curves, staggers and springs. Under reduced motion everything is multiplied by motion.reduced.multiplier (0).
| Token | Value | Use |
|---|---|---|
motion.duration.instant | 0ms | No animation, for reduced motion and instant state swaps. |
motion.duration.fast | 120ms | Hover, focus, toggles. |
motion.duration.base | 240ms | Default UI transition for reveals, menus and accordions. |
motion.duration.slow | 480ms | Page-level transitions, large panels. |
motion.duration.hero | 1200ms | Cinematic hero reveals and shader fades. |
motion.stagger.base | 40ms | Stagger between list/grid children. |
motion.stagger.slow | 80ms | Stagger for hero lines and large cards. |
motion.reduced.multiplier | 0 | Multiply durations by this when prefers-reduced-motion is set (0 = instant). |
| Token | Value | Tailwind |
|---|---|---|
motion.ease.standardDefault easing for UI state changes. | cubic-bezier(0.2, 0, 0, 1) | ease-standard |
motion.ease.out-quartFast start and soft landing, for reveals and enters. | cubic-bezier(0.25, 1, 0.5, 1) | ease-out-quart |
motion.ease.out-expoVery fast start and long settle, for the hero and scroll-linked reveals. | cubic-bezier(0.16, 1, 0.3, 1) | ease-out-expo |
motion.ease.in-outSymmetric, for page transitions and looping motion. | cubic-bezier(0.83, 0, 0.17, 1) | ease-in-out |
motion.ease.anticipatePulls back before overshooting, for playful kinetic accents. | cubic-bezier(0.68, -0.4, 0.32, 1.4) | ease-anticipate |
| Token | stiffness | damping | mass | Use |
|---|---|---|---|---|
motion.spring.snappy | 400 | 30 | 1 | Stiffness of the snappy spring, for UI feedback. |
motion.spring.gentle | 170 | 26 | 1 | Stiffness of the gentle spring, for drifts and layout shifts. |
motion.spring.bouncy | 300 | 12 | 1 | Stiffness of the bouncy spring, for motion that should catch the eye. |
Play each curve at its typical duration. Under reduced motion the end state shows immediately, exactly how the site behaves.
motion.ease.standardDuration: 240ms (motion.duration.base)motion.ease.out-quartDuration: 480ms (motion.duration.slow)motion.ease.out-expoDuration: 480ms (motion.duration.slow)motion.ease.in-outDuration: 480ms (motion.duration.slow)motion.ease.anticipateDuration: 1200ms (motion.duration.hero)Components
Every component reads tokens through Tailwind utilities, so everything here switches theme together with the rest of the page. The name in the corner is the registry item.
Install r/button.jsonInstall r/icon-button.jsonInstall r/link.jsonInstall r/heading.jsondisplay-2We build what others call hard.
h1We build what others call hard.
h2We build what others call hard.
h3We build what others call hard.
h4We build what others call hard.
h5We build what others call hard.
h6We build what others call hard.
Install r/text.jsonLead: the first sentence the reader meets.
Body: calm, readable and never longer than 65 characters per line when measure is on.
Small text for captions and metadata.
Caption in mono for labels.
Install r/eyebrow.jsonInstall r/badge.jsonInstall r/logo-mark.jsonInstall r/avatar.jsonInstall r/container.jsonContainer centres the content and gives it the fluid gutter. Four widths: prose, narrow, default, wide.
size="prose"size="narrow"size="default"size="wide"Install r/section.jsonSection provides vertical rhythm from space.fluid.section.* and a built-in container.
space="sm"space="md"space="lg"Install r/grid.jsonInstall r/grid.jsonInstall r/separator.jsonSeparator: decorative by default, a semantic hr when decorative is false.
Install r/card.jsonCard title
Description under the title.
The card body. Four variants with the same slots: surface, raised, glass and outline.
Install r/card.jsonCard title
Description under the title.
The card body. Four variants with the same slots: surface, raised, glass and outline.
Install r/card.jsonCard title
Description under the title.
The card body. Four variants with the same slots: surface, raised, glass and outline.
Install r/card.jsonCard title
Description under the title.
The card body. Four variants with the same slots: surface, raised, glass and outline.
Install r/card.jsonInstall r/card.jsonCard with the link in its title
The whole card clicks, but the link is named by its title and the rest reads as text.
Install r/input.jsonWe only use it to reply to you.
Install r/field.jsonAn org. no. has nine digits.
Install r/textarea.jsonInstall r/stat.jsonInstall r/skeleton.jsonSkeleton shimmers in motion-safe and holds still under reduced motion.
Install r/marquee.jsonInstall r/motion.jsonReveal: the content is in the HTML, and GSAP animates it in as it scrolls into view.
Install r/motion.jsonA heading that rises line by line.
Install r/motion.jsonInstall r/motion.jsonInstall r/motion.jsonCounter counts up to a value that is in the HTML from the start
Install r/a11y.jsonSkipLink is the first tab stop on every page. Press Tab here to see it.
Skip to contentInstall r/a11y.jsonVisible text – and an invisible part for screen readers.
– and an invisible part for screen readers.Install r/a11y.jsonLiveRegion and announce() tell screen readers something happened without moving focus.
Use it
The design system is built to be used by us, by clients and by agents that write code.
DTCG 2025.10 tokens compiled to CSS variables, a Tailwind v4 theme, JSON and TypeScript. Import the stylesheets, set data-theme, done.
npm config set @hjconsult:registry https://npm.pkg.github.com
pnpm add @hjconsult/tokens@import "tailwindcss";
@import "@hjconsult/tokens/css";
@import "@hjconsult/tokens/theme.css";
@import "@hjconsult/tokens/fluid.css";<html data-theme="nordlys" data-scheme="dark">Every component installs straight into a project with the shadcn CLI. The source is copied to hj/components/…, so you own the code.
npx shadcn add https://hjconsult.no/r/button.jsonDESIGN.md in the token package describes every token, every permutation and the do/don’t list. /r/registry.json lists every item. Both are written to be read by machines as much as by people.
@hjconsult/tokens/design.md@hjconsult/tokens/json/r/registry.json