Skip to content

03 — Design Tokens

Status: 🟢 Deels geïmplementeerd — Brand tokens, semantic tokens, OKLCH shades, dark mode, WCAG contrast, W3C/Tailwind export, token versioning zijn gebouwd. Element tokens (§6 elements) en uitgebreide guardrail UI (§4 visuele feedback) zijn nog vision.

Afhankelijkheden: 01 — Brand Identity (sfeer-presets, wizard data) · 02 — Brand JSON Spec (schema, brand_profiles tabel).

Optionele lagen: 05 — Expansion System en 06 — Guild Taxonomy voegen later extra cascade-lagen toe (Style Packs, Vertical Packs). Dit document werkt volledig zonder die lagen.

Het design token systeem is de technische backbone van Beam’s visuele identiteit. Alles wat er visueel gebeurt — de editor, blocks, addons, AI-gegenereerde content, exports — leest uit één bron: het token schema.


  1. Theming Cascade
  2. Drie-Laags Token Architectuur
  3. Sfeer-preset ↔ Token Schema Alignment
  4. Guardrail Systeem
  5. Semantic Context Laag
  6. Volledig Token Schema
  7. Publieke Token API
  8. Storage Structuur
  9. Export Formaten
  10. Technische Beslissingen
  11. Backlog

Tokens cascaderen van laag naar hoog. Hogere lagen overschrijven lagere.

Laag 3 │ User Overrides (styling builder: handmatige aanpassingen)
│ Altijd de hoogste prioriteit — de gebruiker wint
Laag 2 │ Sfeer-preset
│ "Warm & Aards", "Bold & Expressief", "Zakelijk & Betrouwbaar", etc.
│ = het startpunt dat de styling builder aanbiedt
│ Zie: 01 §7 Sfeer-presets
Laag 1 │ Beam Core Tokens
│ Platform defaults — de basis die altijd geldt
────────┤
Tokens │ W3C Design Token Schema (single source of truth)
│ Alles hierboven resolved naar dit schema

Cascade-regel: Laag 1 is read-only vanuit de styling builder. De builder bewerkt Laag 2 (sfeer-preset keuze) en Laag 3 (user overrides).

Wanneer het guild- en addon-systeem gelanceerd worden, schuiven er twee lagen tussen:

Laag 5 │ User Overrides (zelfde als v1 Laag 3)
Laag 4 │ Vertical Pack Styling (niche-specifieke overrides)
Laag 3 │ Style Packs (guild-gebaseerd, vervangt sfeer-presets)
Laag 2 │ Addon Global Styles (CSS variabelen, fonts van addons)
Laag 1 │ Beam Core Tokens

Dit is backward compatible: bestaande v1-sites behouden hun sfeer-preset als Laag 2, die later 1:1 mapt naar een guild Style Pack.


Brand Tokens → Semantic Tokens → Element Tokens
(ruwe waarden) (betekenis/rol) (toepassing)
#1B4D7A → primary color → button background
Playfair Display → heading font → h1, h2, h3
6px → base radius → card corner

Tokens werken altijd via refs — nooit hardcoded waarden. Wijzig je een brand color → alles downstream past zich automatisch aan via OKLCH shade recalculatie.


3. Sfeer-preset ↔ Token Schema Alignment

Section titled “3. Sfeer-preset ↔ Token Schema Alignment”

De styling builder is het bewerkoppervlak, het token schema is de opslag, en de sfeer-preset is het startpunt. Dezelfde data, drie representaties.

Sfeer-presets: zie 01 — Brand Identity §7. mood_presets tabel: zie 02 — Brand JSON Spec §5.

Sfeer-preset (mood_presets tabel) Token Schema (styling builder)
──────────────────────────────── ────────────────────────────────
colors.primary → tokens.colors.brand.primary
colors.secondary → tokens.colors.brand.secondary
colors.accent → tokens.colors.brand.accent
font_combination → tokens.typography.fonts.heading + body
radius → tokens.shape.radius.preset
spacing → tokens.shape.spacing.preset
shadows → tokens.shape.shadow.preset

Flow: Wizard kiest sfeer-preset → preset tokens worden geladen als startpunt → gebruiker past aan in de styling builder → resultaat opgeslagen als brand_profiles.design_tokens.

Wanneer het guild-systeem gelanceerd wordt, worden sfeer-presets uitgebreid tot Style Packs met extra velden:

Style Pack (toekomst) Extra velden t.o.v. sfeer-preset
───────────────────── ──────────────────────────────────
componentStyles → elements.* (button, table, etc.)
seasonal.overrides → meta.seasonal_overrides
gilde → meta.source_gilde

“You cannot make it ugly” is geen aspiratie — het is een technische implementatie. Guardrails zijn altijd actief, niet optioneel.

RegelImplementatieFeedback
Max 3 brand colorsUI blokkeert 4e kleur”Drie kleuren zijn genoeg voor een sterk merk.”
WCAG AA contrastReal-time check bij elke kleurwijzigingContrast ratio badge: ✅ AA / ⚠️ Bijna / ❌ Onvoldoende
Harmonie checkKleuren buiten OKLCH-gegenereerde schaal worden gewaarschuwd”Deze kleur botst met je palette. Probeer deze suggestie.”
Dark mode contrastAutomatisch gevalideerd bij generatieIdem
RegelImplementatieFeedback
Curated font-parenAlleen vooraf goedgekeurde combinaties heading+bodyFont picker toont compatibele matches
Geen extreme sizesMin/max per element type (H1: 1.5rem–4rem)Slider stopt bij grenzen
Line-height validatieAutomatisch berekend op basis van font sizeNiet handmatig instelbaar tenzij “geavanceerd”
Mobile sizesAutomatisch afgeleid uit desktop (ratio)Preview toont mobiel altijd mee
RegelImplementatieFeedback
Consistente radiusEén radius-preset voor alles (None/Small/Medium/Large/Pill)Geen losse radius per element
Spacing systeemBase unit × vermenigvuldigers, geen vrije waarden (Compact/Comfortable/Spacious)Spacing slider met stappen
Shadow coherentieVier niveaus (None/Subtle/Medium/Prominent), niet vrij instelbaarShadow preset selector

Guardrails voelen niet als restrictie maar als hulp:

SituatieVisueelAudio
Kleur voldoet aan WCAG AASubtiel groen vinkjeZacht “ding”
Kleur faalt contrastKleur schudt licht + suggestie slide-inZacht “bonk”
Font-paar is harmonieusFonts schuiven soepel in positie
Radius verandertAlle elementen animeren smooth mee (spring physics)Subtiel “click” per stap
Kleurwijziging propageertRipple-animatie door alle gerelateerde tokens in sidebarToon (pitch volgt hue)

Tokens zijn contextbewust. Een primary kleur op een donkere achtergrond gebruikt automatisch een lichtere variant:

"primary": {
"default": { "ref": "brand.primary" },
"on-dark": { "ref": "tokens.colors.generated.primary-300" },
"on-light": { "ref": "tokens.colors.generated.primary-700" }
}

Beam lost dit automatisch op op basis van de achtergrondkleur van de container — onzichtbaar voor de gebruiker, ingebakken in het schema.


{
"brand": {
"name": "Bouwbedrijf De Vries",
"logo": {
"url": "https://r2.beam.app/sites/abc123/brand/logo.svg",
"favicon": null
}
},
"tokens": {
"colors": {
"brand": {
"primary": "#1B4D7A",
"secondary": "#F5F0EB",
"accent": "#D4883E"
},
"semantic": {
"primary": {
"default": { "ref": "brand.primary" },
"on-dark": { "ref": "tokens.colors.generated.primary-300" },
"on-light": { "ref": "tokens.colors.generated.primary-700" },
"dark-mode-override": null
},
"background": { "value": "#FFFFFF", "dark-mode-override": "#0F172A" },
"surface": { "value": "#F8F9FA", "dark-mode-override": "#1E293B" },
"text": { "value": "#1A1A2E", "dark-mode-override": "#F1F5F9" },
"text-muted": { "value": "#6B7280", "dark-mode-override": "#94A3B8" },
"border": { "value": "#DEE2E6", "dark-mode-override": "#334155" },
"danger": { "value": "#C92A2A" },
"success": { "value": "#2F9E44" },
"warning": { "value": "#E67700" }
},
"generated": {
"primary-50": "auto (OKLCH)",
"primary-100": "auto (OKLCH)",
"primary-200": "auto (OKLCH)",
"primary-300": "auto (OKLCH)",
"primary-400": "auto (OKLCH)",
"primary-500": "auto (OKLCH)",
"primary-600": "auto (OKLCH)",
"primary-700": "auto (OKLCH)",
"primary-800": "auto (OKLCH)",
"primary-900": "auto (OKLCH)"
}
},
"typography": {
"fonts": {
"heading": { "family": "Roboto Slab", "source": "self-hosted" },
"body": { "family": "Roboto", "source": "self-hosted" },
"mono": { "family": "JetBrains Mono", "source": "self-hosted" }
},
"scale": {
"h1": { "font": "heading", "size": { "desktop": "3rem", "mobile": "2.25rem" }, "weight": 700, "lineHeight": 1.2 },
"h2": { "font": "heading", "size": { "desktop": "2.25rem", "mobile": "1.875rem" }, "weight": 600, "lineHeight": 1.3 },
"h3": { "font": "heading", "size": { "desktop": "1.875rem", "mobile": "1.5rem" }, "weight": 600, "lineHeight": 1.3 },
"h4": { "font": "heading", "size": { "desktop": "1.5rem", "mobile": "1.25rem" }, "weight": 500, "lineHeight": 1.4 },
"h5": { "font": "heading", "size": { "desktop": "1.25rem", "mobile": "1.125rem" }, "weight": 500, "lineHeight": 1.4 },
"h6": { "font": "heading", "size": { "desktop": "1rem", "mobile": "1rem" }, "weight": 500, "lineHeight": 1.4 },
"body": { "font": "body", "size": { "desktop": "1rem", "mobile": "1rem" }, "weight": 400, "lineHeight": 1.6 },
"label": { "font": "body", "size": { "desktop": "0.875rem", "mobile": "0.875rem" }, "weight": 500, "lineHeight": 1.4 },
"caption": { "font": "body", "size": { "desktop": "0.75rem", "mobile": "0.75rem" }, "weight": 400, "lineHeight": 1.4 }
}
},
"shape": {
"radius": {
"preset": "none",
"values": {
"none": "0px",
"small": "4px",
"medium": "8px",
"large": "16px",
"pill": "9999px"
}
},
"spacing": {
"preset": "compact",
"base": "4px",
"values": {
"compact": "4px",
"comfortable": "6px",
"spacious": "8px"
}
},
"shadow": {
"preset": "subtle",
"values": {
"none": "none",
"subtle": "0 1px 2px rgba(0,0,0,0.05)",
"medium": "0 4px 6px rgba(0,0,0,0.07)",
"prominent": "0 10px 15px rgba(0,0,0,0.1)"
}
}
}
},
"elements": {
"button": {
"primary": {
"bg": { "ref": "semantic.primary.default" },
"text": { "value": "#FFFFFF" },
"border": { "value": "transparent" },
"radius": { "ref": "shape.radius.preset" },
"paddingX": "1.25rem",
"paddingY": "0.625rem",
"font": { "ref": "typography.scale.label" },
"states": {
"hover": { "bg": { "ref": "tokens.colors.generated.primary-700" } },
"focus": { "ring": { "ref": "semantic.primary.default" }, "ringOffset": "2px" },
"disabled": { "opacity": 0.5 },
"loading": { "opacity": 0.8 }
}
},
"secondary": {
"bg": { "ref": "semantic.surface" },
"text": { "ref": "semantic.primary.default" },
"border": { "ref": "semantic.border" },
"radius": { "ref": "shape.radius.preset" },
"states": {
"hover": { "bg": { "ref": "tokens.colors.generated.primary-50" } }
}
},
"ghost": {
"bg": { "value": "transparent" },
"text": { "ref": "semantic.primary.default" }
},
"destructive": {
"bg": { "ref": "semantic.danger" },
"text": { "value": "#FFFFFF" }
}
},
"table": {
"headerBg": { "ref": "semantic.surface" },
"borderColor": { "ref": "semantic.border" },
"stripeColor": { "ref": "tokens.colors.generated.primary-50" },
"cellPadding": "0.75rem 1rem"
},
"list": {
"markerColor": { "ref": "semantic.primary.default" },
"itemSpacing": "0.375rem",
"indent": "1.5rem"
},
"input": {
"border": { "ref": "semantic.border" },
"radius": { "ref": "shape.radius.preset" },
"focusRing": { "ref": "semantic.primary.default" },
"bg": { "value": "#FFFFFF" }
},
"badge": {
"radius": { "ref": "shape.radius.pill" },
"font": { "ref": "typography.scale.caption" }
},
"codexCard (toekomst, 05)": {
"bg": { "ref": "semantic.surface" },
"border": { "ref": "semantic.border" },
"radius": { "ref": "shape.radius.lg" },
"titleFont": { "ref": "typography.scale.label" },
"rarityGlow": { "ref": "semantic.primary.default" }
}
},
"meta": {
"version": "1.0",
"onboarding_completed_at": "2026-03-20T12:00:00Z",
"last_published_at": null,
"mood_preset": "solide-strak",
"font_combination": "system-solid",
"token_format": "w3c-design-tokens-draft",
"guardrails": {
"contrast_aa_pass": true,
"color_harmony_pass": true,
"font_pair_validated": true,
"last_checked_at": "2026-03-20T12:00:00Z"
}
}
}

Elk Beam-merk heeft een read-only token endpoint. De pagebuilder, AI content generator, addon blocks en externe tools lezen hier live uit.

Volledige API specificatie: zie 02 — Brand JSON Spec §3.

EndpointBeschrijving
GET /api/sites/{site_id}/tokensVolledig token schema in W3C formaat
GET /api/sites/{site_id}/tokens/cssCSS variables, kant-en-klaar voor embedden in <head>

Tokens zijn de bron — CSS, Tailwind en W3C JSON zijn afgeleiden.


CF R2: beam-media/
sites/
{site_id}/
media/ ← afbeeldingen (onboarding + media library)
fonts/ ← eigen WOFF2 uploads
brand/ ← logo, favicon

Noot over fonts: Curated self-hosted fonts (21 families, OFL-licensed): 19 variable fonts (single woff2, alle weights) + 3 static fallbacks (Poppins, Lato, Proza Libre). De API genereert @font-face CSS via generateFontFaceCss() uit @beam/shared — geen runtime externe font API calls. <link rel="preload"> elimineert FOUT. Eigen WOFF2 uploads worden opgeslagen in R2 (sites/{site_id}/fonts/).


Alle exports zijn afgeleiden van het token schema.

1. CSS Variables

:root {
--color-primary: #1B4D7A;
--color-primary-50: /* OKLCH gegenereerd */;
--font-heading: 'Roboto Slab', serif;
--font-body: 'Roboto', sans-serif;
--radius-base: 0px;
--spacing-base: 4px;
}

2. Tailwind v4 (@theme)

@theme {
--color-primary: #1B4D7A;
--font-heading: 'Roboto Slab';
--font-body: 'Roboto';
--radius-base: 0px;
}

3. W3C Design Tokens JSON Primair formaat. Compatibel met Figma Tokens, Style Dictionary, Canva (via Figma bridge).

Toekomst (05/06): Wanneer het addon-systeem gelanceerd wordt, komt er een vierde export: het Style Pack formaat als distributieformaat voor addons.


OnderwerpBeslissing
Color shade algoritmeOKLCH via culori library
Dark modeAuto-generatie met handmatige override per token
Font bronnenSelf-hosted woff2 in public/fonts/brand/ (19 variable + 3 static, OFL) + eigen WOFF2 upload → R2 (zie 01 §4)
Tailwind exportv4 CSS-first via @theme
Token formaatW3C Design Token spec als primair formaat
PublicerenDraft staat met aparte publiceer stap
VersioningAuto-save + named snapshots (zie 02 §5 token_versions)
Responsive typografieVaste sizes per breakpoint (desktop + mobiel apart, zie 01 §6)
Component statesHover + focus handmatig, disabled + loading automatisch afgeleid
ScopePer site — geen multi-site (agency feature: backlog B9)
Theming cascadev1: 3 lagen (Core → Sfeer-preset → User). Toekomst: 5 lagen met guilds/addons (05/06)
GuardrailsAltijd actief, niet optioneel — kern-feature (zie 01 §8)
Radius presets5 vaste opties: None (0), Small (4), Medium (8), Large (16), Pill (9999)
Spacing presets3 opties: Compact (4px), Comfortable (6px), Spacious (8px)
Shadow presets4 opties: None, Subtle, Medium, Prominent

#FeatureOmschrijving
B1Brand health scoreLive indicator hoe consistent tokens worden toegepast. Hardcoded waarden buiten token systeem verlagen de score.
B2Token importW3C JSON importeren vanuit Figma Tokens, Style Dictionary of andere Beam site
B3”Gebruikt in” impact previewToon waar een token gebruikt wordt vóór je het wijzigt. “Deze kleur zit in 3 pagina’s, 2 buttons, 1 tabel.”
#FeatureOmschrijving
B4Brand lock / permissionsAgency vergrendelt primaire kleur + logo, klant past de rest aan.
B5Changelog / audit trailLeesbare geschiedenis: “Angelo heeft op 14 feb de primary color gewijzigd.”
B6AI stijlgeneratie vanuit URLVoer een inspiratie URL in → Beam extraheert stijl als token startpunt.
B7Animaties & transitionsGlobale instelling: transition snelheid + easing preset.
B8Fluid typographyclamp() per breakpoint als alternatief voor vaste sizes.
#FeatureOmschrijving
B9Agency / multi-site templatesGedeelde token templates over meerdere sites.
B10Real-time samenwerkingMeerdere gebruikers tegelijk in de styling builder.
B11Tokens overdraagbaar naar andere platformsE-mail templates, Canva social media, pitch decks.
B12AI als merkbewakerAI toetst elke nieuwe pagina aan de merkidentiteit.
B13Semantische betekenislaagTokens beschrijven niet alleen hoe iets eruitziet maar wat het betekent. “Blauw = vertrouwen.”

DocumentRelatie
01 — Brand IdentitySfeer-presets (§7), font-combinaties (§4), guardrails (§8), styling builder layout (§6)
02 — Brand JSON SpecBrand JSON schema (§1–2), API endpoints (§3), brand_profiles + token_versions tabellen (§5)
05 — Expansion SystemToekomst: addon tokens, vertical packs
06 — Guild TaxonomyToekomst: Style Packs, guild-gebaseerde presets