Decision
Encapsulate styles with the component instead of decorating markup with utilities. Reach first for scoped styling, Shadow DOM with
idselectors 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 witharia-describedbywhen it relates to another element, than as ansr-onlyutility. 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