Skip to content

Theming

Combat UI theming is driven entirely by CSS custom properties. Everything below is just a token you can override on :root, on a scoped wrapper, or directly on a component.

Theming model

Tokens cascade through normal CSS inheritance. A page-wide override sits on :root; a scoped override sits on any wrapper; a one-off override goes inline on the element. Color scheme follows data-theme="auto|light|dark" set on the document root by cui-theme-toggle.

Colors

Color tokens are written in oklch() for predictable lightness across hue. Every component resolves color through these tokens, so swapping one variable theme an entire surface.

Surface & text

--cui-color-surface
Page and section background
--cui-color-surface-raised
Cards, navbars, popovers
--cui-color-surface-muted
Subtle fills, hover states
--cui-color-border
Hairlines, dividers
--cui-color
Primary text
--cui-color-muted
Secondary text, captions

Brand & feedback

--cui-color-accent
Primary buttons, links, focus
--cui-color-on-accent
Foreground over accent fills
--cui-color-success
Positive state
--cui-color-warning
Caution state
--cui-color-danger
Error and destructive state
--cui-color-focus
Focus ring (defaults to accent)

Inverse surfaces

Inverse tokens are surfaces that flip with theme: light theme exposes dark inverse, dark theme exposes light inverse. Use them for promotional sections, footers, and overlay copy.

--cui-color-inverse-surface
Inverse page background
--cui-color-inverse-surface-raised
Inverse cards
--cui-color-inverse
Inverse text
--cui-color-inverse-muted
Inverse muted text
--cui-color-inverse-border
Inverse hairlines
--cui-color-inverse-eyebrow
Inverse eyebrow / kicker text

Typography

The type scale is fluid: sizes use clamp() so they grow with viewport inline-size. Weights stay constant; line-heights tighten as size grows.

Type scale

Display heading

--cui-font-size-displayclamp(3rem, 1.9rem + 4.4vi, 5.5rem)

Heading 5

--cui-font-size-5clamp(2.375rem, 1.66rem + 2.86vi, 4rem)

Heading 4

--cui-font-size-4clamp(1.875rem, 1.48rem + 1.56vi, 2.75rem)

Heading 3

--cui-font-size-3clamp(1.5rem, 1.28rem + 0.86vi, 2rem)

Lead paragraph

--cui-font-size-2clamp(1.25rem, 1.12rem + 0.52vi, 1.5rem)

Body large

--cui-font-size-1clamp(1.0625rem, 1rem + 0.26vi, 1.1875rem)

Body default

--cui-font-size-0clamp(0.9375rem, 0.9rem + 0.16vi, 1rem)

Caption / metadata

--cui-font-size-neg-1clamp(0.8125rem, 0.79rem + 0.1vi, 0.875rem)

Weights & line-heights

TokenDefaultUse
--cui-font-familyInter, ui-sans-serif, system-ui, …Body and heading face
--cui-font-weight-regular450Default body weight
--cui-font-weight-strong680UI emphasis, eyebrows, buttons
--cui-font-weight-display760Hero / display headings
--cui-line-height1.55Body copy
--cui-line-height-tight1.2Headings & UI
--cui-line-height-display0.98Hero / display
--cui-font-size-eyebrow0.875remSection kickers
--cui-font-size-caption0.875remCaptions
--cui-font-size-metadata0.875remMetadata labels

Spacing

A 7-step rem-based scale. Use it for component padding, gap, and stack rhythm. The bars below are the actual token widths.

--cui-space-1
0.25rem
--cui-space-2
0.5rem
--cui-space-3
0.75rem
--cui-space-4
1rem
--cui-space-5
1.5rem
--cui-space-6
2rem
--cui-space-7
3rem

Section spacing

Section padding is fluid. Pair these with .cui-section via the data-spacing attribute or the cui-section-* modifier classes.

--cui-section-space-compact (clamp(2.5rem, 7vi, 5rem))
--cui-section-space (clamp(4rem, 10vi, 8rem))
--cui-section-space-spacious (clamp(5.5rem, 14vi, 11rem))
--cui-section-space-hero (clamp(6rem, 18vi, 14rem))

Radii & shadows

Radii are intentionally small. --cui-radius-1 is the default control corner; --cui-radius-2 is for surfaces. Two elevation levels keep shadow use disciplined.

Radii

0
--cui-radius-1
--cui-radius-2
999px

Shadows

--cui-shadow-1
--cui-shadow-2

Borders & focus

TokenDefaultUse
--cui-border-size1pxHairlines, table cells
--cui-focus-size2pxFocus ring thickness
--cui-focus-offset2pxFocus ring outset from element
--cui-control-radiusvar(--cui-radius-1)Buttons, inputs

Layout primitives

Layout primitives expose tokens you can override per-instance to reshape stacks, clusters, grids, and splits without writing new selectors.

Containers & measures

TokenDefaultUse
--cui-container72rem.cui-container width
--cui-container-narrow48rem.cui-container-narrow
--cui-container-wide96rem.cui-container-wide
--cui-container-gutterclamp(1rem, 4vi, 2rem)Horizontal page padding
--cui-prose-measure72ch.cui-prose width
--cui-content-measure68chLead text width
--cui-content-size68ch.cui-content width
--cui-center-size100vb.cui-center height
--cui-cover-size100vb.cui-cover height

Stack & cluster

TokenDefaultUse
--cui-stack-gapvar(--cui-space-4)Gap between .cui-stack children
--cui-flow-spacevar(--cui-space-4)Margin between .cui-flow children
--cui-cluster-gapvar(--cui-space-3)Gap between .cui-cluster children
--cui-cluster-aligncenterCross-axis alignment
--cui-cluster-justifyflex-startMain-axis alignment

Grid, switcher, split

TokenDefaultUse
--cui-grid-min16rem.cui-grid minimum column
--cui-grid-gapvar(--cui-space-4)Grid gap
--cui-switcher-gapvar(--cui-space-4).cui-switcher gap
--cui-switcher-threshold40remWidth before switcher splits
--cui-split-gapvar(--cui-space-6).cui-split gap
--cui-split-start1frSplit first column
--cui-split-end1frSplit second column
--cui-split-aligncenterSplit block alignment

Shell & app

TokenDefaultUse
--cui-shell-bar-size4rem.cui-shell-bar minimum height
--cui-app-sidebar-size16rem.cui-app-sidebar width

Density & control scale

--cui-density and --cui-control-scale are registered numeric properties that drive control sizing through --cui-control-size. Drag the slider to see live changes.

1.00
1.00
Bar height = --cui-control-size = 2.5rem
Primary Secondary Tertiary

Buttons consume --cui-control-size as min-block-size, so visible change is bounded by their padding and content. The bar above is sized purely by block-size so you can see the resolved value at a glance.

TokenDefaultUse
--cui-density1Adjusts control vertical density. Values < 1 are denser; > 1 are roomier.
--cui-control-scale1Linear multiplier on top of density.
--cui-control-sizecalc((2.5rem + (var(--cui-density) - 1) * 0.35rem) * var(--cui-control-scale))Resolved control height.
--cui-control-radiusvar(--cui-radius-1)Resolved control radius.

Dark mode

Dark mode is opt-out: prefers-color-scheme: dark applies automatically unless data-theme="light" is set. data-theme="dark" forces dark on light systems. Use cui-theme-toggle to cycle auto / light / dark with persistence.

Dark mode rebinds the surface and text token block. Component tokens that reference these names update transitively, so you rarely need to author dark-specific overrides.

Persistence & FOUC

cui-theme-toggle persists the chosen theme to localStorage under cui-theme and restores it across reloads. It also syncs across tabs via the storage event — change the theme in one tab and other open tabs follow.

Module scripts are deferred, which means the saved theme applies just after first paint. To eliminate that flash, drop a tiny synchronous bootstrap in <head> before the framework module loads:

The snippet only acts on "light" and "dark""auto" is the implicit default, so it doesn't need to set anything.

Component tokens

Each component reads from its own narrow token namespace. Override at the component level to reskin a single instance without affecting siblings.

Button

TokenDefaultEffect
--cui-button-bgvar(--cui-color-surface-muted)Default button background
--cui-button-bordervar(--cui-color-border)Default button border
--cui-button-colorvar(--cui-color)Default button text
--cui-control-size2.5rem (at density 1)Button height
--cui-control-radiusvar(--cui-radius-1)Corner radius

Navbar

TokenDefaultEffect
--cui-navbar-bgvar(--cui-color-surface-raised)Bar background
--cui-navbar-bordervar(--cui-color-border)Bottom hairline
--cui-navbar-gutter2remInline padding
--cui-navbar-widthvar(--cui-container)Inner max width
--cui-navbar-padding-block0.75remVertical padding
--cui-navbar-size3.25remBar minimum height
--cui-navbar-sticky-offset0Sticky top offset
--cui-navbar-sticky-z-index10Sticky stacking context

Hero

TokenDefaultEffect
--cui-hero-min-block-sizemin(86vb, 48rem)Hero minimum height
--cui-hero-padding-blockvar(--cui-section-space-hero)Vertical padding
--cui-hero-gapvar(--cui-space-7)Content / media gap
--cui-hero-content-size42remCopy column width
--cui-hero-media-size38remMedia column width
--cui-hero-media-min-block-sizeclamp(18rem, 44vi, 34rem)Media minimum height
--cui-hero-media-radiusvar(--cui-radius-2)Media corner radius
--cui-hero-overlaylinear-gradient(...)Overlay/background tint
--cui-hero-title-sizevar(--cui-font-size-display)Hero title size
--cui-hero-bgvar(--cui-color-surface)Section background
--cui-hero-colorvar(--cui-color)Foreground

Page intro

TokenDefaultEffect
--cui-page-intro-padding-blockvar(--cui-section-space-compact)Vertical padding
--cui-page-intro-gapvar(--cui-space-5)Content / aside gap
--cui-page-intro-content-size52remContent column max width
--cui-page-intro-aside-size20remAside column width
--cui-page-intro-title-sizevar(--cui-font-size-5)Title size (display in landing variant)
--cui-page-intro-bgvar(--cui-color-surface)Background
--cui-page-intro-colorvar(--cui-color)Foreground
--cui-page-intro-muted-colorvar(--cui-color-muted)Lead / meta color
--cui-page-intro-eyebrow-colorvar(--cui-color-accent)Eyebrow color
--cui-page-intro-inverse-bgvar(--cui-color-inverse-surface)Inverse mode background
--cui-page-intro-inverse-colorvar(--cui-color-inverse-text)Inverse mode foreground
--cui-page-intro-inverse-muted-colorvar(--cui-color-inverse-muted)Inverse muted text
--cui-page-intro-inverse-eyebrow-colorvar(--cui-color-inverse-eyebrow)Inverse eyebrow
--cui-page-intro-inverse-aside-bgvar(--cui-color-inverse-surface-raised)Inverse aside surface

Code

TokenDefaultEffect
--cui-code-bg#111827Code surface
--cui-code-borderhsl(220deg 18% 28%)Frame border
--cui-code-header-bghsl(220deg 22% 12%)Toolbar background
--cui-code-text#dbeafeCode foreground
--cui-code-muted#93a4bcToolbar text
--cui-code-paddingvar(--cui-space-4)Inner padding
--cui-code-tab-size2Tab character width
--cui-code-fontsfmono-regular, consolas, …Code typeface
--cui-code-font-sizevar(--cui-font-size-neg-1)Code size
--cui-code-line-height1.6Line height
--cui-code-comment#7c8da6Comment token
--cui-code-keyword#7dd3fcKeyword token
--cui-code-property#f9a8d4Property token
--cui-code-string#86efacString token
--cui-code-value#fde68aValue/number token
--cui-code-punctuation#cbd5e1Punctuation token
--cui-code-function#c4b5fdFunction/identifier token

Content card & grids

TokenDefaultEffect
--cui-content-card-paddingvar(--cui-space-5)Card padding
--cui-content-card-gapvar(--cui-space-3)Card child gap
--cui-content-card-bgvar(--cui-color-surface-raised)Card background
--cui-content-card-bordervar(--cui-color-border)Card border
--cui-content-card-radiusvar(--cui-radius-2)Card corner radius
--cui-content-card-shadowvar(--cui-shadow-1)Card elevation
--cui-content-card-muted-colorvar(--cui-color-muted)Secondary card text
--cui-grid-min16remAuto-fit grid minimum column (set per content type: 18rem features, 20rem cases, 10rem stats, 9rem logos)
--cui-grid-gapvar(--cui-space-4)Grid gap (or use data-gap)

Feature, case, logo, stat

TokenDefaultEffect
--cui-feature-icon-sizevar(--cui-size-control, 2.5rem)Feature card icon size
--cui-feature-icon-bgvar(--cui-color-surface-muted)Feature icon background
--cui-feature-icon-colorvar(--cui-color-accent)Feature icon foreground
--cui-case-card-media-ratio16 / 10Case card media aspect ratio
--cui-logo-item-min-block-size5remLogo strip cell height
--cui-logo-max-inline-size9remLogo image max width
--cui-logo-max-block-size2.25remLogo image max height
--cui-stat-alignstartStat alignment
--cui-stat-value-sizevar(--cui-font-size-4)Stat figure size