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 type | Pattern | Scope | When to use |
|---|---|---|---|
| Global DS | --wb-ds-{category}--{variant} | Entire design system | Foundation: colors, sizes, spaces |
| Component DS | --wb-ds-{component}__{property} | All instances of a component | Theming: set once, affects all buttons/fields/etc. |
| Instance | --wb-{component}__{property} | One specific element | Overrides: per-element customization |
Never use internal tokens like
--wb-*__{property}--overrideor--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:rootand 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:
- Props (component attributes like
kind="primary") - CSS (custom properties via selectors)
styleprop (inline custom properties)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
innerCssuse 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:
| Name | Width |
|---|---|
xxs | 0px |
xs | 320px |
sm | 576px |
md | 768px |
lg | 992px |
xl | 1280px |
xxl | 1620px |
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
- Inspect the element in browser DevTools
- Navigate to the Computed tab
- Search for the custom property (e.g.,
--wb-button__background) - Click to see which value is applied, the full fallback chain, and where each value is defined
| Symptom | Likely cause | Solution |
|---|---|---|
| Token not applying | Typo in token name | Check spelling matches exactly |
| Color not updating | Palette not generated for custom color | Generate shades or override a built-in brand color |
| State not working | Targeting shadow DOM internals from app CSS | Use :hover on host or put state selectors in theme file |
| All components unstyled | No theme imported or initGlobals not called | Import snow-white and call initGlobals() at startup |
Token Reference
The authoritative token values live in the source files — consult these directly:
| What | File |
|---|---|
| Global tokens (sizes, spacing, fonts, colors) | packages/design/lib/global/_global.scss |
| Snow-white theme values | packages/themes/src/snow-white.scss |
| Size generation mixins | packages/elements/src/size/_size.scss |
| Component tokens (per-component) | packages/elements/src/{component}/_{component}.scss |
| Compatibility theme (migration reference) | packages/themes/src/compatibility.scss |