Experience System

Theming

ThemeProvider, design tokens, color modes, spacing density, and how Tailwind variants connect to the BY theme system.

Theming is how @by/experience-system answers “what does this subtree look like, at what density, in which color mode, and under which product theme key?”—without each component importing a shared JavaScript theme object the way Material UI and legacy CCL LuiThemeProvider flows often did.

Three layers work together:

  1. ThemeProvider — Writes data-ds-theme, data-ds-color, data-ds-spacing, and dir on a wrapper div, and exposes React context for theme, color, spacing, direction, and containerRef.
  2. Design tokens — Mostly CSS custom properties shipped through theme.css (semantic colors, elevation, scales). They are the stable names design and engineering agree on across light and dark.
  3. Tailwind v4 — Utilities and @custom-variant rules that read those attributes and variables, so dark: / compact: and semantic classes stay in sync with the provider.

Iconography is not a third styling layer, but it ships as the @by/icons package alongside @by/experience-system: the same React and token-aware expectations apply, and BY components that show chevrons, alerts, or menu glyphs assume you consume the BY icon set rather than @jda/lui-common-icon-library-mui5. Install @by/icons with the experience system, wire @source for Tailwind if needed (see Installation), and follow by-icons-skill on Skills.

Install order, @source, and optional style.css are documented in Installation. Portal Shell apps should also read Portal theme consumption so themeMessage.themeObject maps cleanly into ThemeProvider.


Token system

Tokens are the vocabulary of the UI: instead of hard-coding hex values in every feature, you reference variables and utilities that theme.css defines for the active data-ds-color and data-ds-spacing. That is how a single Button implementation can respect dark surfaces and compact density without forking source.

Rough groupings:

LayerRoleWhere to read more
Semantic / functionalPage background, surfaces, body text, overlays—what most product chrome should use.Core colors
Palette / scaleStepped ramps (neutral, accent, brand, status) for charts, badges, and deliberate composition.Palette colors,
TypographyType scale, weights, semantic HTML, and monospace—scaled with density where noted.Typography
Spacing densitystandard, compact, ultra-compact change how scaled spacing and typography feel under data-ds-spacing.Spacing
ElevationDrop and inset shadows that respond to theme and color mode.Elevationtheme.css and packages/experience-system/src/theme/tokens/elevation

Prefer semantic names for chrome so when tokens change globally, your screens track without a string replace across hundreds of files. Reach for palette steps when the design language intentionally calls for a scale position (for example heatmaps or data viz).


ThemeProvider

ThemeProvider is a client component. It renders ThemeContext.Provider around a div that carries:

  • data-ds-theme — Set by theme; ThemeProvider applies a branded default so you usually omit theme.
  • data-ds-colorlight or dark (required). Drives which values semantic and palette variables resolve to.
  • data-ds-spacingstandard, compact, or ultra-compact (required). Drives density: scaled spacing steps, typography scaling, and line-height multipliers used across the subtree.
  • dirltr or rtl (optional, default ltr).

If that wrapper is missing, components still mount, but colors, shadows, and scaled gaps may not match the design specification—because the attributes Tailwind variants and internal styles expect are absent.

Basic usage

'use client';

import { ThemeProvider, Button } from '@by/experience-system';

export function App() {
  return (
    <ThemeProvider color="light" spacing="standard" direction="ltr">
      <Button type="button">OK</Button>
    </ThemeProvider>
  );
}

Nested providers

Prefer one ThemeProvider per app shell when the whole tree shares the same theme settings. Nested providers are supported for deliberate islands, such as previews, embedded tool surfaces, or Portal Shell overlays that temporarily change color while keeping the same component system.

Color variants support up to three alternating color scopes:

dark -> light -> dark
light -> dark -> light

Within that limit, light: and dark: Tailwind classes follow the nearest active color scope. For example, a dark app shell can contain a light preferences area, and that area can contain a nested dark portal island without parent dark: utilities leaking into the light subtree.

Same-color nesting, such as dark -> dark, does not change the active color scope. If a UI needs deeper alternating color boundaries, prefer simplifying the provider structure or moving that surface into a separate document boundary, such as an iframe.

Props

PropTypeDefaultDescription
theme'fractal''fractal'Sets data-ds-theme.
color'light' | 'dark'(required)Sets data-ds-color.
spacing'standard' | 'compact' | 'ultra-compact'(required)Sets data-ds-spacing.
direction'ltr' | 'rtl''ltr'Sets dir.
childrenReactNodeSubtree that should see Experience System context.

The package exports matching TypeScript aliases for these values: Theme, ThemeColor, ThemeSpacing, and ThemeDirection, plus ThemeContext for the full value from useThemeContext() (for example import type { ThemeSpacing, ThemeContext } from '@by/experience-system').

useThemeContext

Call useThemeContext() only under ThemeProvider. It returns the current theme, color, spacing, direction, and containerRef pointing at the provider’s wrapper div.

Use it when custom code must branch on color or density, or when a third-party library needs a DOM root scoped to the themed region (portals, measurements, “position relative to this shell”). It throws outside the provider so mistakes fail fast during development.


Tailwind variants

theme.css registers @custom-variant hooks that match data-ds-color, data-ds-theme, and data-ds-spacing. That is how utilities like dark:, fractal:, standard:, compact:, and ultra-compact: stay mechanically aligned with ThemeProvider props—without hand-maintaining parallel boolean flags in every feature.

The light: and dark: variants are boundary-aware for up to three alternating ThemeProvider color scopes. The other variants match the active attribute in the current subtree.

Scaled utilities (names containing scaled) tie into density; see Spacing tokens for how --scale-density and related variables connect to spacing.