Styling Web Components with CSS Custom Properties at Nordhealth

Nordhealth's design system, Nord, relies on Web Components built with Lit. While Shadow DOM encapsulation keeps component styles predictable, it also blocks legitimate user customizations. Nordhealth's solution: expose a styling API through CSS Custom Properties, giving developers fine-grained control without breaking encapsulation.

Building components with Lit

Lit provides the boilerplate—state, scoped styles, and templating—while staying lightweight by building on native JavaScript APIs. This means a lean bundle that takes advantage of existing browser features. The resulting Web Components work across frameworks (or with none at all), and once the main package is loaded, using one is like writing native HTML, distinguished only by the required hyphen in the tag name.

Shadow DOM and style encapsulation

Web Components have a Shadow DOM, a hidden node tree within the element, just like native elements. You can see this in the inspector when you view an input element. This encapsulation is a double-edged sword. Styles written inside the component can't leak out, and page-level or parent-component styles can't leak in. For a component library, this is a feature, not a bug—it guarantees components look as intended regardless of the host page's CSS. Nordhealth reinforces this by adding all: unset; on the :host selector of every component.

But what happens when a user has a legitimate reason to tweak something, like increasing contrast for a specific context or thickening a border? If nothing can pierce the Shadow DOM, how do you unlock that flexibility? CSS Custom Properties are the answer.

CSS Custom Properties as an API

Custom Properties are user-defined CSS properties, prefixed with two hyphens, whose values you apply via the var() function. They behave like any inherited CSS property. Nordhealth applies global design tokens as Custom Properties on the root element through its CSS framework, making those token values available to every element, including Web Components.

That inherited token value is how Nordhealth opens up its components. For certain CSS properties in a component, the stylesheet references a design token directly. For other values—particularly component-specific spacing, colors, or sizing that may not be a design token—Nordhealth defines contextual Custom Properties. For example, instead of hardcoding a value deep in a component's nested styles, the component sets a local custom property at the host level and uses var() for every dependent rule.

This approach yields two benefits. First, it reduces repetition when the same value applies to multiple properties within a component. Second, it makes state and variation changes clean—altering a single Custom Property updates all related styles when, for instance, handling a hover or active state.

The bigger advantage, however, is that these contextual Custom Properties become a public CSS API for the component. Users target the component's selector and redefine a property for a particular instance. The component keeps its style defaults but accepts user overrides with minimal friction. And there's a secondary benefit for components built this way: the component authors can adjust or extend those internal properties without breaking the user's customizations.

The next step: private and public properties

Nordhealth plans to add these contextual properties to the component manifest files already published on npm. That manifest is consumed at docs-build time as data by their Eleventy static site (using the Global Data feature), then rendered as documentation for internal developers.

An ongoing issue is inheritance. If a user wants to restyle two instances of the same component with one declaration, they can't apply the Custom Property on a parent container and have it inherit, because the variables are defined on the component host selector itself. Global design tokens pass through without this issue—they can be intercepted at parent level—but contextual properties are bound to the component boundary.

The planned solution draws on Lea Verou's private custom properties approach: a contextual Custom Property with a default value that gracefully falls back to a public Custom Property. Defined this way, a component retains its default values while permitting a user's overrides at a higher level—via inheritance from a parent element or the stylesheet.

It's a workable compromise between full stop encapsulation and open styling. It's an elegant solution that lets Nordhealth's development team control component appearance where needed while their design system maintains guardrails where it matters.