Skip to content

cv-theme-provider

Provides design tokens as CSS custom properties to descendant components, with support for light, dark, and system-auto color schemes.

Headless: None (UIKit-only component)

Usage

View source
html
<div class="theme-provider-demo-board" data-demo="theme-provider">
  <!-- Basic dark theme. -->
  <cv-theme-provider mode="dark">
    <section class="theme-provider-demo-panel">
      <span>Dark mode scope</span>
      <cv-button variant="primary">Save</cv-button>
    </section>
  </cv-theme-provider>

  <!-- System-auto follows OS light/dark preference. -->
  <cv-theme-provider>
    <section class="theme-provider-demo-panel">
      <span>System color mode</span>
      <cv-badge variant="success" size="small">auto</cv-badge>
    </section>
  </cv-theme-provider>

  <!-- Named themes are registered through the runtime theme engine. -->
  <cv-theme-provider data-theme-demo-brand>
    <section class="theme-provider-demo-panel">
      <span>Named brand theme</span>
      <cv-button variant="primary">Branded</cv-button>
    </section>
  </cv-theme-provider>

  <!-- Nested providers scope overrides to their own subtree. -->
  <cv-theme-provider mode="dark">
    <section class="theme-provider-demo-panel">
      <span>Outer dark provider</span>
      <cv-theme-provider data-theme-demo-sidebar>
        <cv-badge variant="primary" size="small">sidebar scope</cv-badge>
      </cv-theme-provider>
    </section>
  </cv-theme-provider>
</div>

<script type="module">
  import {defineTheme} from '@chromvoid/uikit/theme'

  defineTheme('brand', {
    '--cv-color-primary': '#ff6600',
    '--cv-color-bg': '#1a1a2e',
  })

  defineTheme('sidebar-theme', {
    '--cv-color-primary': '#b388ff',
    '--cv-color-surface': '#181127',
  })

  document.querySelector('[data-theme-demo-brand]')?.setAttribute('theme', 'brand')
  document.querySelector('[data-theme-demo-sidebar]')?.setAttribute('theme', 'sidebar-theme')
</script>

CSS targeting via data attribute

css
cv-theme-provider[data-cv-theme='brand'] {
  --cv-color-accent: #ff9900;
}

Anatomy

<cv-theme-provider> (host, display: contents)
└── <slot>

The element uses display: contents so it does not generate a box in the layout tree. All slotted children inherit CSS custom properties set on the host.

Attributes

AttributeTypeDefaultDescription
themeString""Name of a registered theme to apply via the theme engine
modeString"system"Color scheme mode: light | dark | system

mode behavior

ValueBehavior
lightApplies the light token set. Sets color-scheme: light on the host.
darkApplies the dark token set. Sets color-scheme: dark on the host.
systemListens to prefers-color-scheme via matchMedia and applies the matching token set at runtime.

When mode is system, the provider must:

  1. Query window.matchMedia('(prefers-color-scheme: dark)') on connect.
  2. Add a change listener to update the active scheme when the OS preference changes.
  3. Remove the listener on disconnect.

Slots

SlotDescription
(default)All child content; tokens cascade via CSS custom property inheritance

CSS Parts

None. The provider renders only a <slot> with no wrapper elements.

CSS Custom Properties

The provider defines the full design token surface. All tokens use the --cv- prefix. Tokens are applied either via tokens.css (static import) or via the theme engine (defineTheme + applyTheme) at runtime.

Color tokens

PropertyDark valueLight valueDescription
--cv-color-bg#0b0d12#f8f9fbPage background
--cv-color-surface#141923#ffffffCard / panel background
--cv-color-surface-2#1d2432#f0f2f5Elevated surface level 2
--cv-color-surface-3#242c3d#e6e9eeElevated surface level 3
--cv-color-surface-4#2b3447#dce0e7Elevated surface level 4
--cv-color-surface-elevatedvar(--cv-color-surface-2)var(--cv-color-surface-2)Alias: elevated surface
--cv-color-surface-secondaryvar(--cv-color-surface-2)var(--cv-color-surface-2)Alias: secondary surface
--cv-color-surface-tertiaryvar(--cv-color-surface-3)var(--cv-color-surface-3)Alias: tertiary surface
--cv-color-surface-hovercolor-mix(in oklab, var(--cv-color-primary) 8%, var(--cv-color-surface))color-mix(in oklab, var(--cv-color-primary) 6%, var(--cv-color-surface))Surface hover highlight
--cv-color-text#e8ecf6#1a1f2eDefault text color
--cv-color-text-primaryvar(--cv-color-text)var(--cv-color-text)Alias: primary text
--cv-color-text-muted#9aa6bf#5c6577De-emphasized text
--cv-color-text-secondaryvar(--cv-color-text-muted)var(--cv-color-text-muted)Alias: secondary text
--cv-color-text-subtle#7f8aa3#7a8394Subtle / placeholder text
--cv-color-text-strong#f5f7fc#0e1219Emphasized text
--cv-color-text-strongest#ffffff#000000Maximum contrast text
--cv-color-border#2a3245#d0d5deDefault border color
--cv-color-border-mutedhwb(214 17.3% 63.5% / 0.52)hwb(216 9.8% 72.5% / 0.08)Subtle border
--cv-color-border-strongcolor-mix(in oklab, var(--cv-color-border) 82%, white 18%)color-mix(in oklab, var(--cv-color-border) 82%, black 18%)Strong border
--cv-color-border-accentcolor-mix(in oklab, var(--cv-color-primary) 35%, var(--cv-color-border))color-mix(in oklab, var(--cv-color-primary) 35%, var(--cv-color-border))Accent-tinted border
--cv-color-brandvar(--cv-color-primary)(inherits)Alias: brand color
--cv-color-primary#65d7ff#0e8ab4Primary accent
--cv-color-primary-dark#36bae8#0b7199Darker primary shade
--cv-color-primary-darker#1794c2#085a7aDarkest primary shade
--cv-color-primary-subtlecolor-mix(in oklab, var(--cv-color-primary) 12%, var(--cv-color-surface))color-mix(in oklab, var(--cv-color-primary) 8%, var(--cv-color-surface))Subtle primary tint
--cv-color-primary-mutedcolor-mix(in oklab, var(--cv-color-primary) 22%, var(--cv-color-surface))color-mix(in oklab, var(--cv-color-primary) 15%, var(--cv-color-surface))Muted primary tint
--cv-color-on-primary#03151c#ffffffText on primary background
--cv-color-accent#b388ff#7c3aedSecondary accent (purple)
--cv-color-accent-lightcolor-mix(in oklab, var(--cv-color-accent) 70%, white)color-mix(in oklab, var(--cv-color-accent) 70%, white)Light accent shade
--cv-color-accent-darkcolor-mix(in oklab, var(--cv-color-accent) 70%, black)color-mix(in oklab, var(--cv-color-accent) 70%, black)Dark accent shade
--cv-color-accent-hovercolor-mix(in oklab, var(--cv-color-accent) 85%, white)color-mix(in oklab, var(--cv-color-accent) 85%, black)Accent hover state
--cv-color-accent-contrast#14001f#ffffffText on accent background
--cv-color-cyanvar(--cv-color-primary)(inherits)Alias: cyan
--cv-color-cyan-lightcolor-mix(in oklab, var(--cv-color-cyan) 70%, white)(inherits)Light cyan shade
--cv-color-cyan-darkcolor-mix(in oklab, var(--cv-color-cyan) 70%, black)(inherits)Dark cyan shade
--cv-color-success#6ef7c8#16a367Success color
--cv-color-success-dark#32cca0#0f8553Dark success shade
--cv-color-success-text#e8fff5#052e1aText on success background
--cv-color-warning#ffd36e#b8860bWarning color
--cv-color-warning-dark#d3a74a#9a7209Dark warning shade
--cv-color-warning-text#fff8e6#3d2c04Text on warning background
--cv-color-danger#ff7d86#dc2c3eDanger color
--cv-color-danger-dark#e14f5b#b82232Dark danger shade
--cv-color-danger-text#fff1f2#450a10Text on danger background
--cv-color-infovar(--cv-color-primary)(inherits)Info color
--cv-color-info-textvar(--cv-color-text)var(--cv-color-text)Text on info background
--cv-color-focusvar(--cv-color-primary)(inherits)Focus indicator color
--cv-color-focus-ringvar(--cv-color-primary)(inherits)Focus ring color
--cv-color-hovercolor-mix(in oklab, var(--cv-color-primary) 10%, var(--cv-color-surface))color-mix(in oklab, var(--cv-color-primary) 8%, var(--cv-color-surface))General hover state
--cv-color-activehwb(186 0% 0% / 0.18)hwb(186 0% 20% / 0.14)General active / pressed state
--cv-color-selectedcolor-mix(in oklab, var(--cv-color-primary) 16%, var(--cv-color-surface))color-mix(in oklab, var(--cv-color-primary) 12%, var(--cv-color-surface))Selected item background
--cv-color-overlayrgba(4, 7, 13, 0.72)rgba(15, 20, 30, 0.38)Modal / overlay backdrop

Spacing tokens

PropertyValueDescription
--cv-space-14pxExtra-small space
--cv-space-28pxSmall space
--cv-space-312pxMedium space
--cv-space-416pxDefault space
--cv-space-520pxLarge space
--cv-space-624pxExtra-large space
--cv-space-732px2x-large space
--cv-space-840px3x-large space

Radius tokens

PropertyValueDescription
--cv-radius-16pxBase small radius
--cv-radius-210pxBase medium radius
--cv-radius-314pxBase large radius
--cv-radius-418pxBase extra-large
--cv-radius-svar(--cv-radius-1)Alias: small
--cv-radius-smvar(--cv-radius-1)Alias: small
--cv-radius-mvar(--cv-radius-2)Alias: medium
--cv-radius-mdvar(--cv-radius-2)Alias: medium
--cv-radius-lgvar(--cv-radius-3)Alias: large
--cv-radius-xlvar(--cv-radius-4)Alias: extra-large
--cv-radius-pill999pxPill shape
--cv-radius-full9999pxFull circle

Typography tokens

PropertyValueDescription
--cv-font-family-primary'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serifPrimary font stack
--cv-font-family-bodyvar(--cv-font-family-primary)Body text font
--cv-font-family-display'Satoshi', var(--cv-font-family-primary)Display / heading font
--cv-font-family-sansvar(--cv-font-family-primary)Alias: sans-serif
--cv-font-family-code'JetBrains Mono', 'SF Mono', 'Monaco', 'Consolas', monospaceMonospace font
--cv-font-size-xs0.75remExtra-small text
--cv-font-size-sm0.875remSmall text
--cv-font-size-base1remBase text size
--cv-font-size-mdvar(--cv-font-size-base)Alias: medium
--cv-font-size-lg1.125remLarge text
--cv-font-size-xl1.25remExtra-large text
--cv-font-size-2xl1.5rem2x-large text
--cv-font-size-3xl1.875rem3x-large text
--cv-font-size-4xl2.25rem4x-large text
--cv-font-size-5xl3rem5x-large text
--cv-font-size-6xl3.75rem6x-large text
--cv-font-weight-thin100Thin weight
--cv-font-weight-light300Light weight
--cv-font-weight-normal400Normal weight
--cv-font-weight-regularvar(--cv-font-weight-normal)Alias: regular
--cv-font-weight-medium500Medium weight
--cv-font-weight-semibold600Semi-bold weight
--cv-font-weight-bold700Bold weight
--cv-font-weight-extrabold800Extra-bold weight
--cv-font-weight-black900Black weight

Shadow tokens

PropertyDark valueLight valueDescription
--cv-shadow-sm0 2px 8px rgba(0, 0, 0, 0.24)0 2px 8px rgba(0, 0, 0, 0.08)Small shadow
--cv-shadow-md0 8px 28px rgba(0, 0, 0, 0.32)0 8px 28px rgba(0, 0, 0, 0.12)Medium shadow
--cv-shadow-lg0 16px 48px rgba(0, 0, 0, 0.38)0 16px 48px rgba(0, 0, 0, 0.14)Large shadow
--cv-shadow-xl0 24px 64px rgba(0, 0, 0, 0.42)0 24px 64px rgba(0, 0, 0, 0.16)Extra-large shadow
--cv-shadow-glow0 0 40px var(--cv-color-primary-ring)0 0 40px var(--cv-color-primary-ring)Glow effect
--cv-shadow-1var(--cv-shadow-sm)var(--cv-shadow-sm)Alias: level 1
--cv-shadow-2var(--cv-shadow-md)var(--cv-shadow-md)Alias: level 2
--cv-shadow-3var(--cv-shadow-lg)var(--cv-shadow-lg)Alias: level 3
--cv-shadow-4var(--cv-shadow-xl)var(--cv-shadow-xl)Alias: level 4

Motion tokens

PropertyValueDescription
--cv-duration-instant0msNo transition
--cv-duration-fast120msFast transition
--cv-duration-normal220msStandard transition
--cv-duration-slow320msSlow transition
--cv-duration-slower500msSlower transition
--cv-duration-slowest800msSlowest transition
--cv-easing-standardcubic-bezier(0.2, 0, 0, 1)Standard easing
--cv-easing-acceleratecubic-bezier(0.4, 0, 1, 1)Accelerate easing
--cv-easing-deceleratecubic-bezier(0, 0, 0.2, 1)Decelerate easing
--cv-easing-springcubic-bezier(0.16, 1, 0.3, 1)Spring easing

Z-index tokens

PropertyValueDescription
--cv-z-base0Base layer
--cv-z-overlay1000Overlay layer
--cv-z-modal1100Modal layer
--cv-z-toast1200Toast layer

Sizing tokens

PropertyValueDescription
--cv-size-control-height48pxDefault control height

Visual States

Host selectorDescription
:host([mode="light"])Light color scheme active; color-scheme: light
:host([mode="dark"])Dark color scheme active; color-scheme: dark
:host([mode="system"])Follows OS preference; color-scheme set dynamically

The provider also sets a data-cv-theme attribute on the host element when a named theme is applied via the theme engine. This attribute can be used for CSS targeting:

css
cv-theme-provider[data-cv-theme='my-theme'] {
  /* overrides */
}

Events

None. The theme provider does not emit events. Theme changes propagate via CSS custom property inheritance.

Accessibility

  • No ARIA roles are required. The provider is invisible infrastructure.
  • All color token pairings (text on surface, text on primary, etc.) must meet WCAG AA contrast ratios: 4.5:1 for normal text, 3:1 for large text and UI components.
  • The color-scheme CSS property must be set on the host to ensure native form controls (inputs, selects, scrollbars) render with the correct system appearance.

Theme Engine API

The theme engine (theme-engine.ts) provides a runtime API for registering and applying named themes programmatically. It is independent of the cv-theme-provider element and can target any HTMLElement, ShadowRoot, or Document.

Types

ts
type CVThemeTokenName = `--cv-${string}`
type CVThemeTokens = Record<CVThemeTokenName, string>

interface CVThemeDefinition {
  name: string
  tokens: CVThemeTokens
}

type CVThemeTarget = HTMLElement | ShadowRoot | Document

defineTheme(name: string, tokens: CVThemeTokens): CVThemeDefinition

Registers a named theme in the global theme registry.

  • name must be a non-empty string.
  • All keys in tokens must start with --cv-. Invalid keys throw an Error.
  • Returns a defensive copy of the registered definition.
  • Calling defineTheme with an existing name overwrites the previous definition.

getTheme(name: string): CVThemeDefinition | undefined

Retrieves a registered theme definition by name.

  • Returns undefined if no theme is registered with the given name.
  • Returns a defensive copy; mutations do not affect the registry.

applyTheme(target: CVThemeTarget, name: string): HTMLElement

Applies a registered theme to a target element.

  • Resolves the target: HTMLElement is used directly; Document resolves to document.documentElement; ShadowRoot resolves to shadowRoot.host.
  • Removes all CSS custom properties previously applied to the target by a prior applyTheme call (tracked via a WeakMap).
  • Sets each token as an inline style.setProperty(key, value) on the resolved element.
  • Sets the data-cv-theme attribute on the resolved element to the theme name.
  • Throws an Error if the named theme is not registered.
  • Returns the resolved HTMLElement.

Token prefix rule

All theme tokens must use the --cv- prefix. The engine validates this at registration time and rejects tokens that do not conform.

Light / Dark CSS Cascade Strategy

Light and dark tokens are defined entirely in tokens.css using CSS selectors and a media query — no JavaScript token switching is needed.

1. :root, cv-theme-provider          → dark tokens (default)
2. cv-theme-provider[mode="light"]   → light token overrides
3. @media (prefers-color-scheme: light) {
     :root,
     cv-theme-provider[mode="system"] → light token overrides
   }
mode valueResolution
darkUses the default dark block (no extra selector needed)
lightMatched by [mode="light"] selector
systemMatched by @media + [mode="system"] when OS is light; falls through to dark default when OS is dark

The :root selector inside the @media block handles the no-provider case (bare import 'tokens.css').

The light block overrides only color-varying tokens (colors, shadows, overlay). Scheme-invariant tokens (spacing, radius, typography, motion, z-index, sizing) are defined once in the default block and shared.

ChromVoid UIKit documentation