Donnie D'Amato on stage

Scope Styles to Components, Not Utility Classes

Form follows Function

Keep the styles with the component. Utilities mean that something wasn't considered in the initial design and should be revisited.

On this page

Decision

Encapsulate styles with the component instead of decorating markup with utilities. Reach first for scoped styling, Shadow DOM with id selectors or a CSS-in-JS solution, so an element’s styles live with the element and reference semantic tokens (--ds-surface-navigation-background) rather than literal values. Where a classname is genuinely needed, name it for purpose with a namespace (ds-main-navbar), not for the values it sets. Even the usual holdout, visually hidden screenreader-only text, is better modeled as a property on a text component (<Text screenreaderOnly>), wired up with aria-describedby when it relates to another element, than as an sr-only utility. The aim is a system with no utility-class exceptions to reason about at all.

Alternatives Considered

Utility-class framework (e.g. Tailwind)

πŸ‘ Pros πŸ‘Ž Cons
Fast to compose; every style has a ready-made class
No separate stylesheet to name or maintain
Consistent spacing and color steps out of the box
Classnames encode concrete values, which fights a themable token system
Styling detail lives in the markup, so re-theming means editing every element
Long, repeated class strings for what is usually a missing component

Global, hand-named classnames (BEM and similar)

πŸ‘ Pros πŸ‘Ž Cons
Readable, purpose-driven names Names collide across a codebase, with no scope to keep them apart
Unrelated styles leak into each other

Reasoning

The split that matters is semantic versus prescriptive. A semantic token, or a purpose-named class, describes what an element is for and lets its values change across themes; a prescriptive utility class hard-codes the value and cannot. Scoping styles to the component removes the need for unique global classnames at all, an id inside a shadow root never collides, and keeps each element pointed at intent-named tokens. The recurring complaint that you would otherwise retype a long list of properties is real, but it is usually a signal to make a component, not a reason to spray values across the markup. As the rule goes: if it has a purpose, it should have a name.

Why it mattered

Keeping styling out of the markup is what lets a single component re-theme without a find-and-replace through every view. It is the same bet as naming tokens by intent: describe purpose once, defer the values, and let a scope re-value them later. Utilities optimize the first five minutes of typing; scoped, semantic styles optimize every change after that.

All decisions
Schedule a chat