Last Updated: March 5, 2026
This document describes the architecture of the Design System Starter Kit, including the token system, configuration model, and repository structure.
- Project Overview
- Repository Structure
- Token Architecture
- Three-Axis Configuration Model
- Themes
- Token Naming Convention
- Name:
design-system-starter-kit - Package Scope:
@dsn/*(Design System Name - customizable per organization) - Package Manager: PNPM (with workspaces)
- License: MIT
- Purpose: Production-ready design system starter kit for organizations
- Scalable: Built for growth from day one
- Themeable: Full multi-theme and light/dark mode support
- Accessible: WCAG compliant from the start
- Framework Agnostic: Tokens work everywhere
- Developer Friendly: Clear naming, good DX
- Mobile-First: Responsive design from smallest screens up
design-system-starter-kit/
├── .github/
│ └── workflows/
│ ├── ci.yml # Lint, type-check, test, build
│ └── release.yml
├── .husky/
│ └── pre-commit # Runs lint-staged
├── packages/
│ ├── design-tokens/ # @dsn/design-tokens
│ │ └── src/
│ │ ├── config/
│ │ │ ├── build.js # Multi-config build script
│ │ │ └── config.js # Configuration matrix
│ │ └── tokens/
│ │ ├── themes/
│ │ │ ├── start/
│ │ │ │ ├── base.json # Typography, spacing, sizing, borders, focus
│ │ │ │ ├── colors-light.json # Light mode colors
│ │ │ │ └── colors-dark.json # Dark mode colors
│ │ │ └── wireframe/
│ │ │ ├── base.json # System fonts, minimal borders
│ │ │ ├── colors-light.json # Grayscale colors
│ │ │ └── colors-dark.json # Inverted grayscale
│ │ ├── project-types/
│ │ │ ├── default/
│ │ │ │ └── typography.json # Fluid clamp() font-sizes
│ │ │ └── information-dense/
│ │ │ └── typography.json # Fixed font-sizes
│ │ └── components/
│ │ ├── alert.json
│ │ ├── button.json
│ │ ├── checkbox.json
│ │ ├── checkbox-group.json
│ │ ├── checkbox-option.json
│ │ ├── date-input.json
│ │ ├── date-input-group.json
│ │ ├── form-field.json
│ │ ├── form-field-description.json
│ │ ├── form-field-error-message.json
│ │ ├── form-field-label.json
│ │ ├── form-field-label-suffix.json
│ │ ├── form-field-status.json
│ │ ├── heading.json
│ │ ├── link.json
│ │ ├── note.json
│ │ ├── option-label.json
│ │ ├── ordered-list.json
│ │ ├── paragraph.json
│ │ ├── password-input.json
│ │ ├── radio.json
│ │ ├── radio-group.json
│ │ ├── radio-option.json
│ │ ├── search-input.json
│ │ ├── select.json
│ │ ├── status-badge.json
│ │ ├── text-area.json
│ │ ├── text-input.json
│ │ ├── time-input.json
│ │ └── unordered-list.json
│ ├── core/ # @dsn/core
│ ├── components-html/ # @dsn/components-html
│ ├── components-react/ # @dsn/components-react
│ │ └── scripts/
│ │ └── generate-icons.js # Icon registry generator
│ ├── components-web/ # @dsn/components-web
│ └── storybook/ # Documentation site
│ ├── .storybook/
│ │ ├── main.ts # Storybook config + multilevel-sort
│ │ ├── preview.ts # Decorator, toolbar controls, storySort
│ │ ├── preview-head.html # Dynamic token loading script
│ │ └── preview-body.css # Preview iframe styles
│ └── src/
│ ├── {Component}.stories.tsx # Stories per component
│ ├── {Component}.docs.mdx # MDX docs page (attached to stories)
│ └── {Component}.docs.md # Dutch documentation content
├── docs/
│ ├── README.md # This documentation index
│ ├── 01-architecture.md # This file
│ ├── 02-design-tokens-reference.md
│ ├── 03-components.md
│ ├── 04-development-workflow.md
│ ├── 05-storybook-configuration.md
│ └── changelog.md
├── .eslintrc.js # ESLint config (ignores *.generated.*)
├── tsconfig.json # Root TypeScript config
├── vitest.config.ts # Test configuration
├── package.json
├── pnpm-workspace.yaml
└── LICENSE (MIT)
Decision: No primitive color layer. Hardcoded hex values directly in semantic tokens.
Rationale: Simplicity for v1. Future feature will generate color palettes programmatically.
Purpose: Brand-agnostic, meaningful tokens
Prefix: dsn (Design System Name)
Examples: dsn.color.neutral.*, dsn.space.block.*, dsn.text.font-size.*
Purpose: Component-specific tokens
Reference: Semantic tokens only
Examples: dsn.button.strong.background-color, dsn.button.size.small.font-size
Component CSS → Component Tokens → Semantic Tokens → Values
Why Component Tokens:
- Central definition of component styling
- Easy theming (change tokens, not CSS)
- Multi-brand support
- Consistent with design token standards
- Scalable architecture
Tokens are organized in a three-axis configuration model that enables flexible theming:
| Axis | Values | Affects |
|---|---|---|
| Theme | start, wireframe, ... |
All tokens (typography, spacing, borders, focus, colors) |
| Mode | light, dark |
Only color tokens |
| Project Type | default, information-dense |
Only font-size tokens |
This creates a matrix of configurations: Theme × Mode × Project Type = 8 combinations
start-light-default
start-light-information-dense
start-dark-default
start-dark-information-dense
wireframe-light-default
wireframe-light-information-dense
wireframe-dark-default
wireframe-dark-information-dense
- Theme: Defines the visual identity/branding. Different clients or products can have their own theme while sharing the same component library.
- Mode: Light/dark color schemes. Users can switch modes without changing the theme.
- Project Type: Typography density. Information-dense applications (dashboards, data tables) use fixed font sizes, while marketing sites use fluid typography.
| Token Category | Location | Axis |
|---|---|---|
| Typography (font-family, weight, line-height) | themes/{name}/base.json |
Theme |
| Spacing | themes/{name}/base.json |
Theme |
| Sizing | themes/{name}/base.json |
Theme |
| Borders | themes/{name}/base.json |
Theme |
| Focus | themes/{name}/base.json |
Theme |
| Colors | themes/{name}/colors-{mode}.json |
Theme + Mode |
| Font sizes | project-types/{name}/typography.json |
Project Type |
| Component tokens | components/*.json |
N/A (references above) |
The build system generates all configurations automatically:
// config.js
const themes = ['start', 'wireframe'];
const modes = ['light', 'dark'];
const projectTypes = ['default', 'information-dense'];
// Generates 8 full configurations
for (const theme of themes) {
for (const mode of modes) {
for (const projectType of projectTypes) {
buildConfig(`${theme}-${mode}-${projectType}`, {
sources: [
`themes/${theme}/base.json`,
`themes/${theme}/colors-${mode}.json`,
`project-types/${projectType}/typography.json`,
'components/*.json',
],
});
}
}
}Output:
dist/css/{theme}-{mode}-{projectType}.css(8 files)dist/scss/_{theme}-{mode}-{projectType}.scss(8 files)dist/js/{theme}-{mode}-{projectType}.js(8 files)dist/js/{theme}-{mode}-{projectType}.d.ts(8 files)dist/json/{theme}-{mode}-{projectType}.json(8 files)- Plus backward compatibility aliases
For existing consumers, these aliases are maintained:
| Alias | Points To |
|---|---|
dist/css/variables.css |
start-light-default.css |
dist/css/variables-dark.css |
start-dark-default.css |
dist/scss/_variables.scss |
_start-light-default.scss |
dist/scss/_variables-dark.scss |
_start-dark-default.scss |
dist/js/tokens.js |
start-light-default.js |
dist/js/tokens-dark.js |
start-dark-default.js |
The full-featured default theme with brand colors and polished styling.
- Font: IBM Plex Sans / IBM Plex Mono
- Border radius: 4px (sm), 8px (md), 16px (lg)
- Colors: Full brand palette with blues, greens, reds, oranges
- Focus: Yellow background with dashed outline (GOV.UK style)
A minimal theme for prototyping and development.
- Font: Comic Sans MS (for clear wireframe distinction)
- Border radius: 2px (sm), 4px (md), 8px (lg): more minimal
- Border width: 2px (thin), 4px (medium), 8px (thick): thicker than start
- Colors: Pure black & white: all color scales (accent, action, info, positive, negative, warning) alias to neutral
- Light mode: backgrounds #FFFFFF, text/borders #000000
- Dark mode: backgrounds #000000, text/borders #FFFFFF
- Focus: Yellow (#ffdd00) background with solid black/white outline
-
Create a new folder under
src/tokens/themes/:src/tokens/themes/my-theme/ ├── base.json ├── colors-light.json └── colors-dark.json -
Copy an existing theme as a starting point:
cp -r src/tokens/themes/start src/tokens/themes/my-theme
-
Update
src/config/config.jsto include your theme:const themes = ['start', 'wireframe', 'my-theme'];
-
Customize the token values in your new theme files.
-
Build:
pnpm --filter @dsn/design-tokens build
-
Create a new folder under
src/tokens/project-types/:src/tokens/project-types/my-density/ └── typography.json -
Define font-size tokens (must include all sizes: sm, md, lg, xl, 2xl, 3xl, 4xl):
{ "dsn": { "text": { "font-size": { "sm": { "value": "0.75rem" }, "md": { "value": "0.875rem" }, "lg": { "value": "1rem" }, "xl": { "value": "1.125rem" }, "2xl": { "value": "1.25rem" }, "3xl": { "value": "1.5rem" }, "4xl": { "value": "1.875rem" } } } } } -
Update
src/config/config.js:const projectTypes = ['default', 'information-dense', 'my-density'];
-
Build.
For applications that need to switch themes at runtime, use fetch + style injection for reliable CSS cascade:
let currentStyleElement: HTMLStyleElement | null = null;
async function loadTheme(theme: string, mode: string, projectType: string) {
const configName = `${theme}-${mode}-${projectType}`;
const url = `/path/to/design-tokens/dist/css/${configName}.css`;
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed to load: ${url}`);
const cssText = await response.text();
// Remove existing dynamic style
if (currentStyleElement?.parentNode) {
currentStyleElement.remove();
}
// Create new style element with higher specificity
currentStyleElement = document.createElement('style');
currentStyleElement.setAttribute('data-dsn-tokens', configName);
// Use :root:root for higher specificity to override bundled CSS
currentStyleElement.textContent = cssText.replace(':root', ':root:root');
// Append to end of head for cascade priority
document.head.appendChild(currentStyleElement);
// Dispatch event for components that need to react
window.dispatchEvent(
new CustomEvent('dsn-theme-changed', {
detail: { theme, mode, projectType, configName },
})
);
} catch (error) {
console.error('Theme load error:', error);
}
}
// Examples:
loadTheme('start', 'dark', 'default'); // Dark mode
loadTheme('wireframe', 'light', 'default'); // Wireframe theme
loadTheme('start', 'light', 'information-dense'); // Dense typographyUsing <link> tags for runtime theme switching can cause CSS cascade issues when the application also bundles token CSS via build tools (Vite, webpack). The bundled CSS may load after the dynamic <link>, causing the wrong values to win.
Solution:
- fetch() the CSS file as text
- Inject as
<style>element at the end of<head> - Use
:root:rootselector for higher specificity - MutationObserver (optional) to ensure style stays last if other scripts add styles
This pattern is used in the Storybook configuration (preview-head.html) and ensures reliable theme switching.
{prefix}.{category}.{concept}.{property}.{state}.{variant}
- Nesting depth (general → specific)
- States (active, checked, disabled, focus, focus-visible, hover, visited)
- Variants (inverse, sizes)
- Alphabetical within same level
dsn.color.neutral.bg-default
dsn.color.neutral.bg-hover
dsn.color.neutral.bg-active
dsn.button.strong.background-color
dsn.button.strong.hover.background-color
dsn.button.size.small.font-size
- Design Tokens Reference - All token values and scales
- Components - Component specifications
- Development Workflow - Token update workflow