Skip to main content

Theming

Webbloqs uses CSS custom properties (CSS variables) for all styling. Custom properties pierce the shadow DOM boundary, making them the primary mechanism for theming web components.

Token Architecture

The design system organizes tokens into tiers. When the browser resolves a value, it walks down the fallback chain until it finds one that's set:

Instance directional   --wb-button__bezel-top

Instance general --wb-button__bezel

Component DS dir. --wb-ds-button__bezel-top

Component DS general --wb-ds-button__bezel

Global DS --wb-ds-space--m

In practice, a button's padding resolves through this fallback chain:

var(--wb-button__bezel-top,           // 1. Instance directional
var(--wb-button__bezel, // 2. Instance general
var(--wb-ds-button__bezel-top,// 3. Component DS directional
var(--wb-ds-button__bezel)// 4. Component DS general → Global DS
)
)
)

You don't need to understand every tier to get started. The important distinction is between DS tokens and instance tokens:

Token typePatternScopeWhen to use
Global DS--wb-ds-{category}--{variant}Entire design systemFoundation: colors, sizes, spaces
Component DS--wb-ds-{component}__{property}All instances of a componentTheming: set once, affects all buttons/fields/etc.
Instance--wb-{component}__{property}One specific elementOverrides: per-element customization

Never use internal tokens like --wb-*__{property}--override or --wb-*__{property}--state. These are implementation details — they bypass the fallback chain and will break on upgrades.

For theme authors: Use DS tokens (--wb-ds-*) for 90% of your work. Set them in :root and they apply globally.

For application developers: Use instance tokens (--wb-*) to override specific elements in your code.

Applying Themes

Method 1: Use snow-white as-is

Snow-white is a monochromatic grayscale design system with auto-generated color palettes, responsive typography, and styling for all components. Import it and you're done:

@use "@webbloqs/themes/lib/snow-white";

Method 2: Extend snow-white with brand overrides

This is the most common approach. Import snow-white, then override the brand color — the palette adjusts automatically via CSS color-mix() in OKLCH color space:

@use "@webbloqs/themes/lib/snow-white";

:root, :host {
--wb-ds-color-brand--primary: #2563eb;
}

Snow-white generates 50–950 color palettes (analogous to Tailwind) for eight color names: primary, secondary, accent, gray, error, success, warning, info. Overriding a brand color is enough — you don't need to define each shade.

You can also adjust spacing, typography, and component defaults:

@use "@webbloqs/themes/lib/snow-white";

:root, :host {
// Brand color
--wb-ds-color-brand--primary: #2563eb;

// Spacing
--wb-ds-space--m: var(--wb-ds-size--120);

// Typography
--wb-ds-font-family--default: "Inter", sans-serif;
--wb-ds-font-weight--bold: 700;

// All buttons
--wb-ds-button__border-radius: var(--wb-ds-border-radius--medium);
}

Method 3: Build a theme from scratch

For projects with externally-driven design systems that can't extend snow-white, build a complete theme by importing the design infrastructure and defining all required DS tokens:

@use "@webbloqs/design/lib/media/media";
@use "@webbloqs/design/lib/size/size";

@include media.viewports();

:root, :host {
@include size.scale($scale: 1);

--wb-ds-color-brand--primary: #1a73e8;
--wb-ds-color-primary--default: var(--wb-ds-color-brand--primary);
--wb-ds-font-family--default: "Inter", sans-serif;
--wb-ds-space--m: var(--wb-ds-size--100);

// Must define ALL required --wb-ds-* component tokens
--wb-ds-button__bezel: var(--wb-ds-space--m);
--wb-ds-button__background: var(--wb-ds-color-primary--default);
--wb-ds-field__bezel: var(--wb-ds-space--m);
// ... see compatibility theme for the minimum required set
}

Since webbloqs 4.1.0, components no longer have hardcoded fallback values. You must define all required --wb-ds-* tokens. The compatibility theme source lists the minimum set.

The compatibility theme itself is not a base for building themes — it's a migration shim for upgrading from pre-4.1.0 that provides generic fallback values.

Customizing Components

From weakest to strongest, the customization methods are:

  1. Props (component attributes like kind="primary")
  2. CSS (custom properties via selectors)
  3. style prop (inline custom properties)
  4. innerCss (direct shadow DOM injection — last resort)

Global overrides (DS tokens in :root)

The simplest customization. Set DS tokens in :root to affect all instances of a component:

:root {
--wb-ds-button__background: var(--wb-ds-color-primary--600);
--wb-ds-button__color: var(--wb-ds-color-gray--white);
--wb-ds-button__border-radius: var(--wb-ds-size--100);
}

Instance overrides (CSS selectors)

Target specific elements with instance tokens:

wb-button.danger {
--wb-button__background: var(--wb-ds-color-error--default);
--wb-button__color: var(--wb-ds-color-gray--white);
}

Kind/variant-based styling

Webbloqs buttons support a kind attribute for semantic variants. Define their styles in your theme using @at-root (which escapes Sass nesting):

:root, :host {
// DS-level defaults
--wb-ds-button__bezel: var(--wb-ds-space--m);

// Primary button variant
@at-root wb-button[kind="primary"],
wb-link[button][kind="primary"] {
--wb-button__background: var(--wb-ds-color-primary--dark);
--wb-button__color: var(--wb-ds-color-gray--white);
--wb-button__font-weight: var(--wb-ds-font-weight--bold);

button, a {
&:hover:not(:disabled) {
--wb-button__background: var(--wb-ds-color-primary--default);
}
}
}
}

State selectors (:hover, :focus) must target the internal button or a element because the theme stylesheet is adopted into the shadow root. Application CSS cannot reach these internals — only theme files can.

style prop with tokens

In React, set instance tokens inline:

<Button
style={{
"--wb-button__background": "var(--wb-ds-color-primary--600)",
"--wb-button__border-radius": "var(--wb-ds-border-radius--large)",
}}
>
Custom button
</Button>

innerCss escape hatch

As a last resort, inject CSS directly into a component's shadow DOM:

<Box
innerCss={`
.wb-box::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: var(--wb-ds-color-primary--default);
}
`}
style={{ position: 'relative' }}
>
<p>Content with accent bar</p>
</Box>

Each innerCss use indicates a gap in the token API. Prefer custom properties whenever possible, and consider reporting missing tokens to the webbloqs team.

Shadow DOM Essentials

CSS custom properties are the only reliable way to style shadow DOM from outside.

What works from application CSS:

// Custom properties pierce shadow DOM
wb-button {
--wb-button__background: blue;
}

// Inherited properties work too
wb-button {
color: blue;
font-family: serif;
}

What doesn't work from application CSS:

// Cannot select shadow DOM internals
wb-button .wb-button { // ✗ won't reach into shadow DOM
background: blue;
}

// State selectors on internals need theme context
wb-button button:hover { // ✗ can't reach shadow DOM's <button>
background: blue;
}

For hover/focus states, either target the host element from application CSS:

wb-button:hover {
--wb-button__background: blue;
}

Or target the internal element from a theme file (which gets adopted into the shadow root):

@at-root wb-button {
button:hover:not(:disabled) {
--wb-button__background: blue;
}
}

Responsive Design

Snow-white includes a viewport system with these breakpoints:

NameWidth
xxs0px
xs320px
sm576px
md768px
lg992px
xl1280px
xxl1620px

Use media.at-least() in your theme to adjust tokens at different breakpoints:

@use "@webbloqs/design/lib/media/media";

:root {
--wb-ds-size-scale: 1;

@include media.at-least(md) {
--wb-ds-size-scale: 1.2;
}

@include media.at-least(lg) {
--wb-ds-size-scale: 1.4;
}
}

Changing --wb-ds-size-scale adjusts all dynamic size tokens at once, because they're calculated relative to this value.

Complete Theme Example

A theme that extends snow-white with brand colors, custom buttons, and form field styling:

@use "@webbloqs/themes/lib/snow-white";
@use "@webbloqs/design/lib/media/media";

:root, :host {
// Brand colors
--wb-ds-color-brand--primary: #2563eb;

// Spacing
--wb-ds-space--m: var(--wb-ds-size--120);

// Button defaults
--wb-ds-button__bezel: var(--wb-ds-space--m);
--wb-ds-button__bezel-left: var(--wb-ds-space--l);
--wb-ds-button__bezel-right: var(--wb-ds-space--l);

// Primary button variant
@at-root wb-button[kind="primary"],
wb-link[button][kind="primary"] {
--wb-button__background: var(--wb-ds-color-primary--600);
--wb-button__color: var(--wb-ds-color-gray--white);
--wb-button__border-radius: var(--wb-ds-size--100);
--wb-button__bezel: var(--wb-ds-space--l);

button, a {
&:hover:not(:disabled) {
--wb-button__background: var(--wb-ds-color-primary--700);
}
}
}

// Form fields
@at-root wb-text-field,
wb-dropdown-field {
--wb-ds-field__border-radius: var(--wb-ds-size--75);
--wb-ds-field__border-color: var(--wb-ds-color-gray--300);
--wb-ds-field__bezel: var(--wb-ds-space--l);
}
}

Common Pitfalls

1. Styling shadow DOM internals from application CSS

// ✗ Wrong — .wb-button lives in shadow DOM
.wb-button { background: blue; }

// ✓ Correct — custom properties pierce shadow DOM
wb-button { --wb-button__background: blue; }

2. Adding a new color without generating its palette

Snow-white generates palettes for eight built-in colors. Override --wb-ds-color-brand--primary and the palette re-derives automatically. But if you add a new color name, you must generate the palette yourself:

// ✗ Wrong — no palette for "my-accent"
--wb-ds-color-brand--my-accent: #ff6600;

// ✓ Correct — define shades manually
--wb-ds-color-brand--my-accent: #ff6600;
--wb-ds-color-my-accent--50: color-mix(in oklch, white 95%, var(--wb-ds-color-brand--my-accent));
--wb-ds-color-my-accent--500: var(--wb-ds-color-brand--my-accent);
// ... through --950

3. Mixing static and dynamic size scales

Borders use static sizes (fixed pixels). Typography and spacing use dynamic sizes (scale with --wb-ds-size-scale):

// ✗ Wrong — dynamic size for border width
--wb-ds-border-width--default: var(--wb-ds-size--10);

// ✓ Correct — static size for border width
--wb-ds-border-width--default: var(--wb-ds-size-static--10); // 1px

4. State selectors in application styles

// ✗ Wrong — can't reach shadow DOM's <button>
wb-button { button:hover { --wb-button__background: blue; } }

// ✓ Correct — target host element
wb-button:hover { --wb-button__background: blue; }

// ✓ Also correct — in theme file (adopted into shadow root)
@at-root wb-button {
button:hover:not(:disabled) { --wb-button__background: blue; }
}

Debugging

  1. Inspect the element in browser DevTools
  2. Navigate to the Computed tab
  3. Search for the custom property (e.g., --wb-button__background)
  4. Click to see which value is applied, the full fallback chain, and where each value is defined
SymptomLikely causeSolution
Token not applyingTypo in token nameCheck spelling matches exactly
Color not updatingPalette not generated for custom colorGenerate shades or override a built-in brand color
State not workingTargeting shadow DOM internals from app CSSUse :hover on host or put state selectors in theme file
All components unstyledNo theme imported or initGlobals not calledImport snow-white and call initGlobals() at startup

Token Reference

The authoritative token values live in the source files — consult these directly:

WhatFile
Global tokens (sizes, spacing, fonts, colors)packages/design/lib/global/_global.scss
Snow-white theme valuespackages/themes/src/snow-white.scss
Size generation mixinspackages/elements/src/size/_size.scss
Component tokens (per-component)packages/elements/src/{component}/_{component}.scss
Compatibility theme (migration reference)packages/themes/src/compatibility.scss