Field Guide / intermediate

Custom properties: make design decisions visible in CSS

Custom properties are most valuable when they expose the decisions a component allows callers to change.

Custom properties are not just variables. They participate in the cascade, inherit by default, and can be changed at runtime without recompiling CSS. That makes them useful for component contracts.

A good custom property answers: what decision should be configurable from outside this component?

Keep the contract small

.callout {
  --callout-accent: #0f8b8d;
  --callout-surface: #e8f3f3;
  border-inline-start: 0.35rem solid var(--callout-accent);
  background: var(--callout-surface);
  padding: 1rem;
}

.callout[data-tone="warning"] {
  --callout-accent: #d85d3f;
  --callout-surface: #f8e8e2;
}

The component exposes tone decisions, not every declaration. A caller can change the accent and surface without rewriting the component’s spacing, border side, or layout.

Prefer semantic names at boundaries

Inside a component, --accent may be readable. Across a codebase, it becomes ambiguous. --callout-accent or --button-bg communicates scope. Global design tokens should describe the design system value, while component properties should describe the component slot.

:root {
  --color-danger: #d85d3f;
}

.alert {
  --alert-accent: var(--color-danger);
}

This gives you two layers of meaning: the brand palette and the component decision.

Fallbacks are for resilience, not silence

.meter {
  inline-size: var(--meter-value, 0%);
}

Fallbacks are useful when a missing value has a safe default. They are harmful when they hide a broken integration. If a component cannot render correctly without a property, make that requirement visible in the API or markup instead of masking it.

Runtime behavior is the point

Because custom properties resolve in the browser, they pair well with media queries, container queries, user preferences, and small interactive tools.

@media (prefers-reduced-motion: reduce) {
  :root {
    --motion-duration: 1ms;
  }
}

That is more maintainable than trying to remember every selector that has a transition. The decision lives where the browser can apply it.

References