Skip to content

Brand Identity & Design Token Systeem

Het Brand Identity systeem in Beam stelt gebruikers in staat de visuele identiteit van hun site te definiëren via een Styling Builder op /brand. Het systeem genereert automatisch een compleet design token pakket (CSS custom properties, semantic tokens, shade scales, dark mode) op basis van de keuzes van de gebruiker.

Architectuur:

┌──────────────────────────────────────────────────────────┐
│ Dashboard (/brand) │
│ ┌─────────┬──────────┬───────────┬──────────┬──────────┐ │
│ │ Presets │ Kleuren │Typografie │Elementen │ Export │ │
│ └────┬────┴────┬─────┴─────┬────┴────┬─────┴────┬─────┘ │
│ │ │ │ │ │ │
│ └─────────┴───────────┴─────────┘ │ │
│ │ │ │
│ DesignTokens │ │
│ │ │ │
│ PUT /brand/:siteId │ │
│ │ │ │
├─────────────────────┼────────────────────────────┼────────┤
│ API (Hono Worker) │ │ │
│ ▼ │ │
│ brand_profiles │ │
│ (design_tokens JSONB) │ │
│ │ │ │
│ ┌──────┴──────┐ │ │
│ ▼ ▼ │ │
│ auto-snapshot GET /tokens/css ◄──────┘ │
│ (trigger) GET /tokens (W3C) │
│ │ GET /tokens/tailwind │
│ ▼ │
│ token_versions │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ Public Site (Astro SSR) │
│ <link href="/brand/{siteId}/tokens/css" rel="stylesheet">│
│ → Alle brand tokens beschikbaar als CSS custom properties│
└──────────────────────────────────────────────────────────┘

Alle design tokens worden opgeslagen als één design_tokens JSONB veld in brand_profiles. De TypeScript interface:

interface DesignTokens {
colors: BrandColors // 7 kleuren
fontCombination: FontCombinationId // 1 van 12 font-paren
typography: TypographyTokens // Fijnafstelling
radius: RadiusPreset // Hoekafronding
shadow: ShadowPreset // Schaduw
gradients?: BrandGradient[] // Optionele gradients
}
VeldDoelCSS Variabele
primaryHoofdkleur — knoppen, links, accenten--brand-primary
secondarySecundaire kleur — headers, hover states--brand-secondary
accentAccentkleur — highlights, badges--brand-accent
neutralNeutrale kleur — borders, muted tekst, achtergronden--brand-neutral
surfaceAchtergrondkleur — pagina achtergrond, kaarten--brand-surface
textDarkDonkere tekstkleur--brand-text-dark
textLightLichte tekstkleur--brand-text-light

Elke kleur is een hex waarde (#RRGGBB). Validatie via Zod: z.string().regex(/^#[0-9a-fA-F]{6}$/).

VeldBereikStandaardDoel
headingWeight100–900700Font weight voor headings
headingLetterSpacing-0.05em tot 0.05em-0.02emLetter-spacing headings
headingLineHeight0.8–1.51.2Line-height headings
bodyWeight300–700400Font weight voor body tekst
bodyLineHeight1.2–2.01.75Line-height body tekst
scalecompact/default/dramaticdefaultType scale voor heading sizes
Scaleh1 mobile/desktoph2h3h4
compact1.75rem/2.25rem1.5rem/1.875rem1.25rem/1.5rem1.125rem/1.25rem
default2rem/2.5rem1.75rem/2rem1.5rem/1.75rem1.25rem/1.5rem
dramatic2.5rem/3.5rem2rem/2.5rem1.5rem/1.875rem1.25rem/1.5rem

Per brand kleur (primary, secondary, accent) worden 10 shades gegenereerd via de OKLCH kleurruimte:

--brand-primary-50 (lightest)
--brand-primary-100
--brand-primary-200
--brand-primary-300
--brand-primary-400
--brand-primary-500 (≈ oorspronkelijke kleur)
--brand-primary-600
--brand-primary-700
--brand-primary-800
--brand-primary-900 (darkest)

Functie: generateShadeScale(hex) in packages/shared/src/color-utils.ts.

Automatisch afgeleid van de brand kleuren:

CSS VariabeleBronDoel
--brand-surfacecolors.surfacePagina achtergrond
--brand-surface-altprimary-50 shadeAlternatieve achtergrond
--brand-borderneutral-300 of fallbackStandaard borders
--brand-border-lightneutral-200 of fallbackSubtiele borders
--brand-text-primarycolors.textDarkPrimaire tekst
--brand-text-mutedneutral-500 of fallbackGrijze/gedempte tekst
--brand-on-primaryContrast berekeningTekst op primary achtergrond
--brand-on-secondaryContrast berekeningTekst op secondary achtergrond
--brand-on-accentContrast berekeningTekst op accent achtergrond
--brand-dangerVast: #DC2626Foutmeldingen
--brand-successVast: #059669Succesmeldingen
--brand-warningVast: #D97706Waarschuwingen
--brand-infoVast: #2563EBInformatie

Functie: generateSemanticTokens(colors) in packages/shared/src/color-utils.ts.

Automatisch berekend via OKLCH:

@media (prefers-color-scheme: dark) {
:root {
--brand-surface: /* donkere variant */
--brand-surface-alt: /* donkere variant */
--brand-border: /* donkere variant */
--brand-border-light: /* donkere variant */
--brand-text-primary: /* lichte variant */
--brand-text-muted: /* gedempte lichte variant */
}
}

Functie: generateDarkModeTokens(colors) in packages/shared/src/color-utils.ts.

Per brand kleur wordt een contrast-kleur berekend voor tekst op gekleurde achtergronden:

--brand-primary-contrast: #111827 of #ffffff
--brand-secondary-contrast: #111827 of #ffffff
--brand-accent-contrast: #111827 of #ffffff

De Styling Builder heeft 5 tabs met volledig WAI-ARIA tabs pattern:

TabComponentProps
PresetsPresetPickerselectedPreset, onSelectPreset
KleurenColorsTabcolors, onColorsChange
TypografieTypographyTabselectedFont, typography, onFontChange, onTypographyChange
ElementenElementsTabradius, shadow, onRadiusChange, onShadowChange
ExportExportTabsiteId

Keyboard navigatie: ArrowLeft/ArrowRight navigeert tussen tabs, Home/End gaat naar eerste/laatste tab.

  • Preset paletten: 6 sfeer-presets als klikbare kaarten met 5-kleur preview
  • Vrije kleurkiezer: 7 ColorInput componenten met:
    • Native <input type="color"> picker
    • Hex text input met live validatie
    • Inline WCAG contrast badge per kleur (ratio + pass/fail)
  • WCAG contrast checking: Client-side via checkWcagContrast() — geen API call nodig. Badge toont groen (AA/AAA) of oranje (fail) met ratio. Bij fail wordt een suggestie voor een accessible alternatief berekend via suggestAccessibleColor()
  • Kleurvarianten preview: OKLCH shade scale (50-900) per brand kleur als interactieve strip. Hover toont shade nummer + hex. Click kopieert hex naar clipboard. CSS var naam beschikbaar in tooltip
  • Font-paren: 12 combinaties als klikbare kaarten met heading + body font preview
  • Fijnafstelling: 5 slider controls (heading weight, letter-spacing, line-height, body weight, body line-height)
  • Type scale: 3 opties (compact/standaard/dramatisch) als button group

3 export formaten:

FormaatEndpointContent-Type
CSS Custom PropertiesGET /brand/:siteId/tokens/csstext/css
W3C Design Tokens JSONGET /brand/:siteId/tokensapplication/json
Tailwind v4 @themeGET /brand/:siteId/tokens/tailwindtext/css

Elke kaart heeft een “Kopieer” (clipboard) en “Open” (nieuw tabblad) knop.

Modal (VersionHistoryModal) met:

  • Snapshot opslaan: Label input + opslaan knop. Max 100 karakters
  • Versie-lijst: Chronologisch met relatieve timestamps, auto/handmatig label
  • Herstellen: Per versie een restore knop die de tokens terugzet en de pagina ververst

Automatische snapshots worden aangemaakt door een PostgreSQL trigger bij elke update op brand_profiles. Max 50 auto-snapshots per site (FIFO cleanup).

MethodPathBeschrijving
GET/brand/:siteIdBrand profile ophalen (auto-create als niet bestaat)
PUT/brand/:siteIdDesign tokens updaten
GET/brand/:siteId/versionsVersiegeschiedenis ophalen (limit 50)
POST/brand/:siteId/versionsNamed snapshot aanmaken
POST/brand/:siteId/versions/:id/restoreSnapshot herstellen (met audit log)
MethodPathBeschrijving
GET/brand/:siteId/tokens/cssCSS custom properties stylesheet
GET/brand/:siteId/tokensW3C Design Tokens JSON
GET/brand/:siteId/tokens/tailwindTailwind v4 @theme CSS
GET/brand/:siteId/contrastWCAG contrast rapport

Caching: CSS endpoint gebruikt CF edge cache (caches.default) met max-age=60, s-maxage=300. Cache wordt automatisch gepurged bij brand profile updates via purgeCache().

KolomTypeBeschrijving
idUUIDPK
site_idUUIDFK → sites, unique
mood_presettextGeselecteerde sfeer-preset
design_tokensJSONBVolledige DesignTokens object
onboarding_dataJSONBOnboarding metadata
created_attimestamptz
updated_attimestamptz
KolomTypeBeschrijving
idUUIDPK
site_idUUIDFK → sites
design_tokensJSONBSnapshot van tokens
labeltextOptioneel label (handmatige snapshots)
autobooleantrue = auto-snapshot door trigger
created_byUUIDGebruiker die snapshot maakte
created_attimestamptz

RLS: Team members (via get_team_owner_id()) hebben volledige toegang. INSERT enforceert created_by = auth.uid() of NULL.

Brand tokens worden real-time gesynchroniseerd via twee mechanismen:

  1. BroadcastChannel — Cross-tab sync binnen dezelfde browser sessie
  2. Supabase Realtime — Multi-user sync bij gelijktijdig bewerken

Hook: useBrandRealtimeSync() in apps/dashboard/src/lib/hooks/use-brand-realtime.ts. Past CSS variabelen direct toe op document.documentElement via applyBrandCssVars().

De publieke site (Astro SSR) laadt brand tokens als stylesheet:

<link rel="stylesheet" href="https://api.builtwithbeam.com/brand/{siteId}/tokens/css" />

Dit levert een compleet CSS bestand met:

  1. Self-hosted @font-face regels (variable fonts + static fallbacks, geen externe CDN)
  2. :root CSS custom properties (alle brand + semantic + shade tokens)
  3. Typography regels (heading/body font-family, weight, line-height, letter-spacing)
  4. Heading sizes per type scale (mobile-first + desktop breakpoint)
  5. @media (prefers-color-scheme: dark) overrides

Alle design token logica zit in packages/shared/src/:

FunctieBestandBeschrijving
getDefaultDesignTokens()mood-presets.tsStandaard tokens (eerste preset)
getMoodPreset(id)mood-presets.tsPreset ophalen op ID
generateShadeScale(hex)color-utils.tsOKLCH 50-900 shade scale
generateSemanticTokens(colors)color-utils.tsAfgeleide semantic tokens
generateDarkModeTokens(colors)color-utils.tsDark mode overrides
checkWcagContrast(fg, bg)color-utils.tsWCAG AA/AAA check + ratio
suggestAccessibleColor(fg, bg)color-utils.tsAccessible alternatief zoeken
getContrastRatio(fg, bg)color-utils.tsRaw contrast ratio
isLightColor(hex)color-utils.tsLicht/donker detectie
generateFontFaceCss(family)font-manifest.tsSelf-hosted @font-face CSS genereren (variable of static)
getFontPreloadPaths(family)font-manifest.tsFont preload paden ophalen
getFontCombination(id)mood-presets.tsFont-paar ophalen

Bestaande database records kunnen de neutral en surface velden missen (toegevoegd in maart 2026). De mergeTokensWithDefaults() helper in BrandPage.tsx vult ontbrekende velden aan met defaults:

function mergeTokensWithDefaults(dbTokens: Partial<DesignTokens>): DesignTokens {
const defaults = getDefaultDesignTokens()
return {
colors: {
...defaults.colors,
...dbTokens.colors,
neutral: dbTokens.colors?.neutral ?? defaults.colors.neutral,
surface: dbTokens.colors?.surface ?? defaults.colors.surface,
},
// ... overige velden met defaults
}
}

De API Zod schema’s markeren neutral en surface als optioneel om backwards compatibiliteit te garanderen.

  • Zod validatie op alle write-inputs (kleuren, typography, gradients)
  • Defense-in-depth: DB-data wordt opnieuw gevalideerd met Zod vóór CSS generatie
  • Font selectie via enum: Geen vrije font-invoer, alleen 12 voorgedefinieerde combinaties
  • safeFontName(): Regex strip van \'"(){};/<> karakters voor CSS embedding
  • Gradient whitelist: SAFE_GRADIENT_RE accepteert alleen linear-gradient/radial-gradient met hex kleuren
  • RLS: Alle queries via user-scoped Supabase client, admin client alleen voor publieke read-only endpoints
  • Rate limiting: 60 req/min per IP op publieke endpoints
  • Edge cache purge: Automatisch bij brand profile updates

Zie Security Standards voor de volledige checklist.