3 Topics
Chapter•110 Minutes

Building Design Systems and Theme Architectures

Create semantic design tokens, reusable component variants, themeable CSS custom properties, accessible states, and maintainable design-system foundations.

Semantic Tokens and Design-System Foundations

A design token represents a reusable design decision such as a color, spacing value, font size, border radius, shadow, or motion duration.

Tokens can be organized into primitive and semantic layers. Primitive tokens represent raw values, while semantic tokens describe how those values are used. For example, a primitive color might be blue-600, while a semantic token might be color-action-primary.

Semantic naming makes themes and redesigns easier because components depend on intent rather than a specific color name. If the brand palette changes, the semantic mapping can change without rewriting every component.

Sass maps can store compile-time tokens, while CSS custom properties expose runtime values to the browser.

Primitive and Semantic Tokens
$palette: (
  blue-600: #2457c5,
  gray-900: #171717,
  gray-100: #f5f5f5
);

:root {
  --color-action-primary: #{map.get($palette, blue-600)};
  --color-text-primary: #{map.get($palette, gray-900)};
  --color-surface: #{map.get($palette, gray-100)};
}

.button-primary {
  background: var(--color-action-primary);
  color: white;
}

.page {
  color: var(--color-text-primary);
  background: var(--color-surface);
}
💡 This example uses map.get() to map primitive Sass colors to semantic CSS custom properties. It requires @use "sass:map"; at the beginning of the file. Components depend on semantic properties such as --color-action-primary. A new brand palette can be introduced by changing the token mapping rather than editing every component.
Knowledge Check

Why are semantic design tokens often more maintainable than using raw color names throughout components?

Light and Dark Themes

Theme architecture should separate the semantic role of a value from its actual color. A surface token may be light in one theme and dark in another, while the component continues using the same property name.

CSS custom properties are a strong choice for runtime theme switching. A theme can be selected through an attribute or class on a root element, and all descendants can inherit the corresponding tokens.

Sass can generate the initial token declarations or organize theme data, but the browser handles runtime theme selection. For system preference support, consider the prefers-color-scheme media feature.

Test focus indicators, borders, disabled states, charts, illustrations, and shadows in both themes. Dark mode is not simply an inversion of every color.

Runtime Theme Switching
:root,
[data-theme="light"] {
  color-scheme: light;
  --surface: #ffffff;
  --text: #202020;
  --border: #dddddd;
}

[data-theme="dark"] {
  color-scheme: dark;
  --surface: #171717;
  --text: #f5f5f5;
  --border: #444444;
}

.panel {
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
}
💡 The theme attribute selects a set of custom-property values. The panel does not need separate component-specific selectors for every theme because it consumes semantic properties. The color-scheme declaration informs the browser about supported native UI color schemes. Explicitly test native form controls and other interface elements in each supported theme.
Knowledge Check

Which feature is especially useful for changing theme values without recompiling Sass?

Component Variants, States, and Accessibility

A reusable component should have clear base styles, supported variants, and interaction states. Sass maps and loops can generate predictable variant classes, while mixins can standardize recurring focus and responsive behavior.

Interactive states include hover, focus-visible, active, disabled, loading, success, and error. Not every component requires every state, but supported states should be defined consistently.

Accessibility should be considered in the underlying CSS design. Keyboard focus must remain visible, text contrast should be sufficient, and information should not be conveyed through color alone. Disabled and loading states should also be reflected appropriately in the component's HTML and application logic.

Keep component classes stable and avoid selectors that depend excessively on the exact internal markup hierarchy.

Reusable Button Variants
$button-variants: (
  primary: #2457c5,
  success: #218838,
  danger: #c62828
);

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  border: 0;
  border-radius: 0.5rem;
  font: inherit;
  cursor: pointer;

  &:focus-visible {
    outline: 3px solid #222;
    outline-offset: 3px;
  }

  &:disabled {
    opacity: 0.55;
    cursor: not-allowed;
  }
}

@each $name, $color in $button-variants {
  .button--#{$name} {
    background: $color;
    color: white;
  }
}
💡 The base class defines common layout and interaction styles. The loop creates one visual variant for every map entry. A real design system should also validate contrast for each variant and account for hover, active, and focus states. HTML button elements should use the actual disabled attribute when the action is unavailable.
Knowledge Check

Which pseudo-class is particularly appropriate for showing a keyboard focus indicator?