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
Brand & feedback
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.
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
Heading 5
Heading 4
Heading 3
Lead paragraph
Body large
Body default
Caption / metadata
Weights & line-heights
| Token | Default | Use |
|---|---|---|
--cui-font-family | Inter, ui-sans-serif, system-ui, … | Body and heading face |
--cui-font-weight-regular | 450 | Default body weight |
--cui-font-weight-strong | 680 | UI emphasis, eyebrows, buttons |
--cui-font-weight-display | 760 | Hero / display headings |
--cui-line-height | 1.55 | Body copy |
--cui-line-height-tight | 1.2 | Headings & UI |
--cui-line-height-display | 0.98 | Hero / display |
--cui-font-size-eyebrow | 0.875rem | Section kickers |
--cui-font-size-caption | 0.875rem | Captions |
--cui-font-size-metadata | 0.875rem | Metadata 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
--cui-space-2
--cui-space-3
--cui-space-4
--cui-space-5
--cui-space-6
--cui-space-7
Section spacing
Section padding is fluid. Pair these with .cui-section via the data-spacing
attribute or the cui-section-* modifier classes.
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
Shadows
Borders & focus
| Token | Default | Use |
|---|---|---|
--cui-border-size | 1px | Hairlines, table cells |
--cui-focus-size | 2px | Focus ring thickness |
--cui-focus-offset | 2px | Focus ring outset from element |
--cui-control-radius | var(--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
| Token | Default | Use |
|---|---|---|
--cui-container | 72rem | .cui-container width |
--cui-container-narrow | 48rem | .cui-container-narrow |
--cui-container-wide | 96rem | .cui-container-wide |
--cui-container-gutter | clamp(1rem, 4vi, 2rem) | Horizontal page padding |
--cui-prose-measure | 72ch | .cui-prose width |
--cui-content-measure | 68ch | Lead text width |
--cui-content-size | 68ch | .cui-content width |
--cui-center-size | 100vb | .cui-center height |
--cui-cover-size | 100vb | .cui-cover height |
Stack & cluster
| Token | Default | Use |
|---|---|---|
--cui-stack-gap | var(--cui-space-4) | Gap between .cui-stack children |
--cui-flow-space | var(--cui-space-4) | Margin between .cui-flow children |
--cui-cluster-gap | var(--cui-space-3) | Gap between .cui-cluster children |
--cui-cluster-align | center | Cross-axis alignment |
--cui-cluster-justify | flex-start | Main-axis alignment |
Grid, switcher, split
| Token | Default | Use |
|---|---|---|
--cui-grid-min | 16rem | .cui-grid minimum column |
--cui-grid-gap | var(--cui-space-4) | Grid gap |
--cui-switcher-gap | var(--cui-space-4) | .cui-switcher gap |
--cui-switcher-threshold | 40rem | Width before switcher splits |
--cui-split-gap | var(--cui-space-6) | .cui-split gap |
--cui-split-start | 1fr | Split first column |
--cui-split-end | 1fr | Split second column |
--cui-split-align | center | Split block alignment |
Shell & app
| Token | Default | Use |
|---|---|---|
--cui-shell-bar-size | 4rem | .cui-shell-bar minimum height |
--cui-app-sidebar-size | 16rem | .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.
--cui-control-size = 2.5rem
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.
| Token | Default | Use |
|---|---|---|
--cui-density | 1 | Adjusts control vertical density. Values < 1 are denser; > 1 are roomier. |
--cui-control-scale | 1 | Linear multiplier on top of density. |
--cui-control-size | calc((2.5rem + (var(--cui-density) - 1) * 0.35rem) * var(--cui-control-scale)) | Resolved control height. |
--cui-control-radius | var(--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
| Token | Default | Effect |
|---|---|---|
--cui-button-bg | var(--cui-color-surface-muted) | Default button background |
--cui-button-border | var(--cui-color-border) | Default button border |
--cui-button-color | var(--cui-color) | Default button text |
--cui-control-size | 2.5rem (at density 1) | Button height |
--cui-control-radius | var(--cui-radius-1) | Corner radius |
Navbar
| Token | Default | Effect |
|---|---|---|
--cui-navbar-bg | var(--cui-color-surface-raised) | Bar background |
--cui-navbar-border | var(--cui-color-border) | Bottom hairline |
--cui-navbar-gutter | 2rem | Inline padding |
--cui-navbar-width | var(--cui-container) | Inner max width |
--cui-navbar-padding-block | 0.75rem | Vertical padding |
--cui-navbar-size | 3.25rem | Bar minimum height |
--cui-navbar-sticky-offset | 0 | Sticky top offset |
--cui-navbar-sticky-z-index | 10 | Sticky stacking context |
Hero
| Token | Default | Effect |
|---|---|---|
--cui-hero-min-block-size | min(86vb, 48rem) | Hero minimum height |
--cui-hero-padding-block | var(--cui-section-space-hero) | Vertical padding |
--cui-hero-gap | var(--cui-space-7) | Content / media gap |
--cui-hero-content-size | 42rem | Copy column width |
--cui-hero-media-size | 38rem | Media column width |
--cui-hero-media-min-block-size | clamp(18rem, 44vi, 34rem) | Media minimum height |
--cui-hero-media-radius | var(--cui-radius-2) | Media corner radius |
--cui-hero-overlay | linear-gradient(...) | Overlay/background tint |
--cui-hero-title-size | var(--cui-font-size-display) | Hero title size |
--cui-hero-bg | var(--cui-color-surface) | Section background |
--cui-hero-color | var(--cui-color) | Foreground |
Page intro
| Token | Default | Effect |
|---|---|---|
--cui-page-intro-padding-block | var(--cui-section-space-compact) | Vertical padding |
--cui-page-intro-gap | var(--cui-space-5) | Content / aside gap |
--cui-page-intro-content-size | 52rem | Content column max width |
--cui-page-intro-aside-size | 20rem | Aside column width |
--cui-page-intro-title-size | var(--cui-font-size-5) | Title size (display in landing variant) |
--cui-page-intro-bg | var(--cui-color-surface) | Background |
--cui-page-intro-color | var(--cui-color) | Foreground |
--cui-page-intro-muted-color | var(--cui-color-muted) | Lead / meta color |
--cui-page-intro-eyebrow-color | var(--cui-color-accent) | Eyebrow color |
--cui-page-intro-inverse-bg | var(--cui-color-inverse-surface) | Inverse mode background |
--cui-page-intro-inverse-color | var(--cui-color-inverse-text) | Inverse mode foreground |
--cui-page-intro-inverse-muted-color | var(--cui-color-inverse-muted) | Inverse muted text |
--cui-page-intro-inverse-eyebrow-color | var(--cui-color-inverse-eyebrow) | Inverse eyebrow |
--cui-page-intro-inverse-aside-bg | var(--cui-color-inverse-surface-raised) | Inverse aside surface |
Code
| Token | Default | Effect |
|---|---|---|
--cui-code-bg | #111827 | Code surface |
--cui-code-border | hsl(220deg 18% 28%) | Frame border |
--cui-code-header-bg | hsl(220deg 22% 12%) | Toolbar background |
--cui-code-text | #dbeafe | Code foreground |
--cui-code-muted | #93a4bc | Toolbar text |
--cui-code-padding | var(--cui-space-4) | Inner padding |
--cui-code-tab-size | 2 | Tab character width |
--cui-code-font | sfmono-regular, consolas, … | Code typeface |
--cui-code-font-size | var(--cui-font-size-neg-1) | Code size |
--cui-code-line-height | 1.6 | Line height |
--cui-code-comment | #7c8da6 | Comment token |
--cui-code-keyword | #7dd3fc | Keyword token |
--cui-code-property | #f9a8d4 | Property token |
--cui-code-string | #86efac | String token |
--cui-code-value | #fde68a | Value/number token |
--cui-code-punctuation | #cbd5e1 | Punctuation token |
--cui-code-function | #c4b5fd | Function/identifier token |
Content card & grids
| Token | Default | Effect |
|---|---|---|
--cui-content-card-padding | var(--cui-space-5) | Card padding |
--cui-content-card-gap | var(--cui-space-3) | Card child gap |
--cui-content-card-bg | var(--cui-color-surface-raised) | Card background |
--cui-content-card-border | var(--cui-color-border) | Card border |
--cui-content-card-radius | var(--cui-radius-2) | Card corner radius |
--cui-content-card-shadow | var(--cui-shadow-1) | Card elevation |
--cui-content-card-muted-color | var(--cui-color-muted) | Secondary card text |
--cui-grid-min | 16rem | Auto-fit grid minimum column (set per content type: 18rem features, 20rem cases, 10rem stats, 9rem logos) |
--cui-grid-gap | var(--cui-space-4) | Grid gap (or use data-gap) |
Feature, case, logo, stat
| Token | Default | Effect |
|---|---|---|
--cui-feature-icon-size | var(--cui-size-control, 2.5rem) | Feature card icon size |
--cui-feature-icon-bg | var(--cui-color-surface-muted) | Feature icon background |
--cui-feature-icon-color | var(--cui-color-accent) | Feature icon foreground |
--cui-case-card-media-ratio | 16 / 10 | Case card media aspect ratio |
--cui-logo-item-min-block-size | 5rem | Logo strip cell height |
--cui-logo-max-inline-size | 9rem | Logo image max width |
--cui-logo-max-block-size | 2.25rem | Logo image max height |
--cui-stat-align | start | Stat alignment |
--cui-stat-value-size | var(--cui-font-size-4) | Stat figure size |