Skip to content

CSS Custom Properties

CSS custom properties are the standard way to let consumers theme a Lit component without breaking Shadow DOM encapsulation. This lesson covers how to define, consume, and document them well.

Custom Properties Cross the Shadow Boundary

Unlike ordinary CSS rules, CSS custom properties (--my-color: blue;) are inherited values that cascade down through the DOM tree, including across Shadow DOM boundaries. This makes them the intended mechanism for exposing themeable values from inside a component's static styles, letting page-level CSS set a value that the component then reads with var(--my-color).

Because the component author controls exactly which custom properties are read (and can provide sensible fallback values), this stays fully compatible with encapsulation — consumers can only influence the specific values the component author deliberately exposed.

// Inside the component's static styles:
static styles = css`
  button {
    background: var(--button-bg, #2563eb);
    color: var(--button-fg, white);
    border-radius: var(--button-radius, 6px);
  }
`;

// From the consumer's page-level CSS:
my-button {
  --button-bg: #16a34a;
  --button-radius: 999px;
}

The component defines three themeable custom properties with sensible fallbacks; the consumer overrides two of them without touching the component's internal markup or Shadow DOM at all.

var() with Fallback Values

var(--custom-property-name, fallback-value)
  • Always provide a fallback value as the second argument to var() so the component looks correct even if the consumer sets nothing.
  • Custom property names are conventionally kebab-case and often prefixed with the component name, e.g. --my-card-radius.
  • Custom properties set on an ancestor (even far up the page) cascade down through any number of nested Shadow DOM boundaries.
  • Unlike regular CSS properties, custom properties can hold any value — a color, a length, or even a comma-separated list.

Custom Properties Cheat Sheet

Conventions for a clean, documented theming API.

Practice Example
Prefix with component name --my-card-radius, not just --radius
Always provide a fallback var(--my-card-radius, 8px)
Document every exposed property A table listing name, purpose, and default
Group related tokens --my-card-bg, --my-card-fg, --my-card-border
Set at a high-level ancestor for app-wide theming :root { --my-card-bg: #111; }

Designing a Component's Theming API

Treat your exposed custom properties as a deliberate, documented part of your component's public API — not every internal CSS value needs to be themeable. Expose only the properties that genuinely make sense for consumers to override, and give each one a clear, prefixed name.

static styles = css`
  :host {
    --_internal-computed-gap: calc(var(--card-gap, 1rem) * 2);
  }
  .card {
    gap: var(--_internal-computed-gap);
    background: var(--card-bg, white);
    box-shadow: var(--card-shadow, 0 1px 3px rgba(0,0,0,0.1));
  }
`;

An internal, non-public custom property (conventionally prefixed with an underscore) can build on a public one without exposing every intermediate computation.

Theming at Different Scopes

Custom properties can be set at any level: globally on :root for app-wide theming, on a specific container to theme just that section, or directly on one component instance for a one-off override.

Scope Where Set Effect
App-wide :root { --card-bg: #111; } Every component in the app inherits this value
Section-wide .dark-section { --card-bg: #000; } Only components inside .dark-section
Single instance <my-card style="--card-bg: red"> Only this one element

Common Mistakes

  • Forgetting to provide a fallback value in var(), causing broken/blank styling for consumers who don't set the custom property.
  • Exposing every single internal CSS value as a public custom property, making the theming API overwhelming and hard to maintain.
  • Not prefixing custom property names, risking collisions with another component's identically-named property.
  • Assuming custom properties automatically theme deeply nested, unrelated third-party components the same way — they only affect properties the component itself reads with var().

Key Takeaways

  • CSS custom properties cross the Shadow DOM boundary by design, unlike ordinary style rules.
  • Always pair var(--name, fallback) with a sensible default value.
  • Design and document a deliberate, prefixed set of themeable custom properties as part of your component's public API.
  • Custom properties can be scoped globally, per-section, or per-instance depending on where they're set.

Pro Tip

Maintain a documented table of every custom property your component exposes, right alongside its properties and events documentation — consumers building a design system on top of your components will look for exactly this table first.