Skip to content
HJ ConsultHJ Consult
esc
Start a brief
Services
Work
Field notes
About
Brand
Norsk
Englishen
Switch mode: Nordlys
Switch mode: Systems
↑↓ select, ↵ open, esc close⌘K
Mode

Living brand book

The design system, live

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.

310 tokens from @hjconsult/tokens v0.1.0Themes: Northern lights and Systems

Principles

Three rules we design by

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.

Voice and tone

Bokmål

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.

English

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.

Write
  • “We set up monitoring in phase one.”
  • “Your data stays in the EU.”
  • “We don’t know that yet. We’ll find out in the brief.”
  • “We build it, and you own the code.”
Don’t write
  • “We deliver innovative, scalable synergies.”
  • “Contact us for a no-obligation quote!!!”
  • “Enterprise-grade AI transformation.”
  • “We don’t just write code – we create value.”

Motion

  1. 01

    Content first

    Everything exists in the HTML before JavaScript runs. Motion adds, and never reveals something that would otherwise be hidden.

  2. 02

    Tokens, not numbers

    Durations from motion.duration.*, curves from motion.ease.*, springs from motion.spring.*. No magic milliseconds in components.

  3. 03

    Respect reduced motion

    prefers-reduced-motion zeroes every duration and drops parallax and shader animation. Everything has a static version.

  4. 04

    Enter with expo, move with standard

    Elements arrive fast and land softly (out-expo). Movement uses the standard curve. Loops use in-out.

Accessibility

  1. 01

    At least WCAG 2.2 AA

    Text on canvas ≥ 7:1 and muted text ≥ 4.5:1, tested for every theme and colour scheme at build time.

  2. 02

    Visible focus

    color.focus with a thick ring and a 2 px offset. We never remove outlines.

  3. 03

    Semantics before ARIA

    Landmarks, one h1 per page, real buttons and links. ARIA fills gaps and is never a patch.

  4. 04

    Keyboard and screen reader

    Everything is reachable with Tab. Decorative effects are aria-hidden, and animated text keeps the real string in the DOM.

Colour

Semantic colours that follow the theme

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.

Surfaces

TokenUseNorthern lightsSystems
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 only1.0:1Decorative only
color.surface-raised--hj-color-surface-raised
Elevated surface: popovers, sticky bars, hovered cards.1.1:1Decorative only1.1:1Decorative only

Text

TokenUseNorthern lightsSystems
color.fg--hj-color-fg
Primary text and icons. ≥ 7:1 on canvas in every theme and scheme.17.4:1AAA18.1:1AAA
color.fg-muted--hj-color-fg-muted
Secondary text: metadata, descriptions. ≥ 4.5:1 on canvas.7.9:1AAA8.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:1AA5.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.accent14.4:1AAA13.2:1AAA
color.accent-alt-fg--hj-color-accent-alt-fg
Text and icons placed on accent-alt fills.vs color.accent-alt7.0:1AAA14.8:1AAA

Accent

TokenUseNorthern lightsSystems
color.accent--hj-color-accent
Primary brand accent: links, primary buttons, highlights.14.4:1AAA13.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:1AAA10.1:1AAA
color.accent-soft--hj-color-accent-soft
Accent at ~15 % alpha: selected rows, soft badges, tints.1.4:1Decorative only1.3:1Decorative only
color.accent-alt--hj-color-accent-alt
Secondary accent: data visualisation, tags, the second hero colour.7.0:1AAA14.8:1AAA
color.focus--hj-color-focus
Focus ring colour. Pair with border.width.thick and a 2px offset.14.4:1AAA13.2:1AAA

Lines and glass

TokenUseNorthern lightsSystems
color.line--hj-color-line
Hairline dividers and default borders (alpha, works over any surface).1.2:1Decorative only1.2:1Decorative only
color.line-strong--hj-color-line-strong
Emphasised borders: inputs, table headers, hard rules.1.5:1Decorative only1.8:1Decorative only
color.glass--hj-color-glass
Translucent fill for glass panels. Pair it with backdrop-filter.1.1:1Decorative only1.1:1Decorative only
color.glass-line--hj-color-glass-line
Border for glass panels.1.4:1Decorative only1.5:1Decorative only

Status

TokenUseNorthern lightsSystems
color.positive--hj-color-positive
Success and confirmation: text-safe on canvas.9.1:1AAA9.3:1AAA
color.warning--hj-color-warning
Caution and pending: text-safe on canvas.10.3:1AAA10.6:1AAA
color.danger--hj-color-danger
Errors and destructive actions: text-safe on canvas.6.7:1AA6.9:1AA

Hero palette

TokenUseNorthern lightsSystems
color.hero.a--hj-color-hero-a
Shader / illustration palette stop A: the base (darkest or lightest field).1.0:1Decorative only1.2:1Decorative only
color.hero.b--hj-color-hero-b
Shader / illustration palette stop B: the mid-tone wash.4.0:1Large text7.3:1AAA
color.hero.c--hj-color-hero-c
Shader / illustration palette stop C: the accent glow.14.4:1AAA14.9:1AAA
color.hero.d--hj-color-hero-d
Shader / illustration palette stop D: the contrasting flare.4.7:1AA14.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

One grotesk, one mono, one fluid scale

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.

Specimen: Displayvar(--font-schibsted), "Schibsted Grotesk", ui-sans-serif, system-ui, sans-serif

Sphinx of black quartz, judge my vow.

Æ Ø Å æ ø å 0123456789 “quote” → ← ↑ ↓ 12 °C

Specimen: Bodyvar(--font-schibsted), "Schibsted Grotesk", ui-sans-serif, system-ui, sans-serif

Blå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

Specimen: Monovar(--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);
}

Fluid scale

RoleSpecimenMinMaxLine-heightTrackingTailwind
display-1Aa Ææ Øø Åå3rem8rem1.05-0.03emtext-display-1
display-2Aa Ææ Øø Åå2.5rem5.5rem1.05-0.03emtext-display-2
h1Aa Ææ Øø Åå2.25rem4rem1.05-0.015emtext-h1
h2Aa Ææ Øø Åå1.75rem2.75rem1.15-0.015emtext-h2
h3Aa Ææ Øø Åå1.375rem1.875rem1.15-0.015emtext-h3
h4Aa Ææ Øø Åå1.125rem1.375rem1.30text-h4
h5Aa Ææ Øø Åå1rem1.125rem1.30text-h5
h6Aa Ææ Øø Åå0.875rem1rem1.30.08emtext-h6
leadAa Ææ Øø Åå1.125rem1.375rem1.650text-lead
bodyAa Ææ Øø Åå1rem1.125rem1.50text-body
smallAa Ææ Øø Åå0.875rem0.9375rem1.50text-small
captionAa Ææ Øø Åå0.75rem0.8125rem1.50text-caption

Tokens

Space, radius, shadow and motion

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.

Space

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.

TokenValueSpaceTailwind
space.px--hj-space-px1pxp-px
space.0-5--hj-space-0-50.125remp-0.5
space.1--hj-space-10.25remp-1
space.2--hj-space-20.5remp-2
space.3--hj-space-30.75remp-3
space.4--hj-space-41remp-4
space.5--hj-space-51.25remp-5
space.6--hj-space-61.5remp-6
space.8--hj-space-82remp-8
space.10--hj-space-102.5remp-10
space.12--hj-space-123remp-12
space.16--hj-space-164remp-16
space.20--hj-space-205remp-20
space.24--hj-space-246remp-24
space.32--hj-space-328remp-32
space.40--hj-space-4010remp-40
space.48--hj-space-4812remp-48
space.64--hj-space-6416remp-64
space.fluid.gutter.*--hj-space-gutter1rem → 2.5remPage gutter at the sm breakpoint (16px).px-gutter
space.fluid.section.sm.*--hj-space-section-sm2.25rem → 5remCompact section padding on a phone (36px).py-section-sm
space.fluid.section.md.*--hj-space-section-md3.25rem → 8remDefault section padding on a phone (52px).py-section-md
space.fluid.section.lg.*--hj-space-section-lg4.5rem → 12remHero/feature section padding on a phone (72px).py-section-lg

Radius

From xs for chips to 3xl for large panels. full gives pills and circles.

  • radius.xs2pxrounded-xs
  • radius.sm4pxrounded-sm
  • radius.md6pxrounded-md
  • radius.lg8pxrounded-lg
  • radius.xl12pxrounded-xl
  • radius.2xl16pxrounded-2xl
  • radius.3xl24pxrounded-3xl
  • radius.full9999pxrounded-full

Shadow

Three levels plus glow. The glow colour follows the theme accent.

  • shadow.1shadow-1
  • shadow.2shadow-2
  • shadow.3shadow-3
  • shadow.glowshadow-glow

Motion

Durations, curves, staggers and springs. Under reduced motion everything is multiplied by motion.reduced.multiplier (0).

Durations

TokenValueUse
motion.duration.instant0msNo animation, for reduced motion and instant state swaps.
motion.duration.fast120msHover, focus, toggles.
motion.duration.base240msDefault UI transition for reveals, menus and accordions.
motion.duration.slow480msPage-level transitions, large panels.
motion.duration.hero1200msCinematic hero reveals and shader fades.
motion.stagger.base40msStagger between list/grid children.
motion.stagger.slow80msStagger for hero lines and large cards.
motion.reduced.multiplier0Multiply durations by this when prefers-reduced-motion is set (0 = instant).

Curves

TokenValueTailwind
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

Springs

TokenstiffnessdampingmassUse
motion.spring.snappy400301Stiffness of the snappy spring, for UI feedback.
motion.spring.gentle170261Stiffness of the gentle spring, for drifts and layout shifts.
motion.spring.bouncy300121Stiffness of the bouncy spring, for motion that should catch the eye.

See the curves

Play each curve at its typical duration. Under reduced motion the end state shows immediately, exactly how the site behaves.

  • Curvemotion.ease.standardDuration: 240ms (motion.duration.base)
  • Curvemotion.ease.out-quartDuration: 480ms (motion.duration.slow)
  • Curvemotion.ease.out-expoDuration: 480ms (motion.duration.slow)
  • Curvemotion.ease.in-outDuration: 480ms (motion.duration.slow)
  • Curvemotion.ease.anticipateDuration: 1200ms (motion.duration.hero)

Components

All of @hjconsult/ui, in its main variants

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.

  • ButtonInstall r/button.json
  • IconButtonInstall r/icon-button.json
  • LinkInstall r/link.json
  • HeadingInstall r/heading.json
    display-2

    We build what others call hard.

    h1

    We build what others call hard.

    h2

    We build what others call hard.

    h3

    We build what others call hard.

    h4

    We build what others call hard.

    h5

    We build what others call hard.

    h6

    We build what others call hard.

  • TextInstall r/text.json

    Lead: 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.

  • EyebrowInstall r/eyebrow.json
    LabelWith dot07With indexAccent
  • BadgeInstall r/badge.json
    NeutralAccentAlternativeRunningDelayedDown
  • LogoMarkInstall r/logo-mark.json
    HJ ConsultHJ ConsultHJ ConsultHJ Consult
  • AvatarInstall r/avatar.json
  • ContainerInstall r/container.json

    Container centres the content and gives it the fluid gutter. Four widths: prose, narrow, default, wide.

    size="prose"size="narrow"size="default"size="wide"
  • SectionInstall r/section.json

    Section provides vertical rhythm from space.fluid.section.* and a built-in container.

    space="sm"space="md"space="lg"
  • GridInstall r/grid.json
    Cell 1
    Cell 2
    Cell 3
    Cell 4
    Cell 5
    Cell 6
  • BentoInstall r/grid.json
    Spans 8 of 12
    Spans 4
    Spans 4
    Spans 8
  • SeparatorInstall r/separator.json

    Separator: decorative by default, a semantic hr when decorative is false.

  • Card (surface)Install r/card.json

    Card title

    Description under the title.

    The card body. Four variants with the same slots: surface, raised, glass and outline.

    surfaceFooter
  • Card (raised)Install r/card.json

    Card title

    Description under the title.

    The card body. Four variants with the same slots: surface, raised, glass and outline.

    raisedFooter
  • Card (glass)Install r/card.json

    Card title

    Description under the title.

    The card body. Four variants with the same slots: surface, raised, glass and outline.

    glassFooter
  • Card (outline)Install r/card.json

    Card title

    Description under the title.

    The card body. Four variants with the same slots: surface, raised, glass and outline.

    outlineFooter
  • Card (interactive)Install r/card.json
  • Card (link)Install r/card.json
  • Field + InputInstall r/input.json
    Optional

    We only use it to reply to you.

  • Field (error)Install r/field.json

    An org. no. has nine digits.

  • Field + TextareaInstall r/textarea.json

  • StatInstall r/stat.json
    2Sample value
    16Sample valueexample
    0Sample valuesteady
  • SkeletonInstall r/skeleton.json

    Skeleton shimmers in motion-safe and holds still under reduced motion.

  • MarqueeInstall r/marquee.json
    Next.jsSupabaseVippsStripeArduino OptaMistral
  • RevealInstall r/motion.json
    010203

    Reveal: the content is in the HTML, and GSAP animates it in as it scrolls into view.

  • SplitHeadingInstall r/motion.json

    A heading that rises line by line.

  • MagneticInstall r/motion.json
  • ParallaxInstall r/motion.json
    Parallax: scroll past me.
  • CounterInstall r/motion.json
    329

    Counter counts up to a value that is in the HTML from the start

  • SkipLinkInstall r/a11y.json

    SkipLink is the first tab stop on every page. Press Tab here to see it.

    Skip to content
  • VisuallyHiddenInstall r/a11y.json

    Visible text – and an invisible part for screen readers.

    – and an invisible part for screen readers.
  • LiveRegion with announce()Install r/a11y.json

    LiveRegion and announce() tell screen readers something happened without moving focus.

Use it

Tokens as a package, components as a registry

The design system is built to be used by us, by clients and by agents that write code.

@hjconsult/tokens

DTCG 2025.10 tokens compiled to CSS variables, a Tailwind v4 theme, JSON and TypeScript. Import the stylesheets, set data-theme, done.

  1. 1. Point @hjconsult at GitHub Packages and install (access on request)
    npm config set @hjconsult:registry https://npm.pkg.github.com
    pnpm add @hjconsult/tokens
  2. 2. Import the stylesheets after Tailwind
    @import "tailwindcss";
    @import "@hjconsult/tokens/css";
    @import "@hjconsult/tokens/theme.css";
    @import "@hjconsult/tokens/fluid.css";
  3. 3. Pick theme and scheme on the html element
    <html data-theme="nordlys" data-scheme="dark">
shadcn registry

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.json

For agents

DESIGN.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