skip to content

You own a design system's CSS consumed by several product teams. How would you define and enforce the contract for how product code is allowed to override it, and what breaks if you leave that contract implicit?

level: principalimportance: should knowfreq 28%

answer

  1. designed path or discovered path
  2. reserve a layer for consumers
  3. configuration absorbs the predictable cases
  4. their escalation is permanent, yours is not
  5. layer names are versioned API

basics

~20 s

Publish an explicit override mechanism — a declared cascade-layer order with a layer reserved for product code, zero-weight defaults, and custom properties for values meant to be configured — and enforce it with linting. Left implicit, teams discover it by escalating selectors, and the escalation is permanent.

solid answer

~50 s

An override contract answers three questions in writing: where product overrides go, what they are guaranteed to beat, and what is off-limits. Concretely I publish a cascade-layer order that reserves a layer above the system's for product code, ship the system's visual defaults at zero weight so any product class beats them, and expose the values I expect people to change as custom properties so most cases need no cascade contest at all. Then I enforce it — a linted specificity ceiling and a ban on `!important` outside one allowlisted file — and version the layer names as public API. If the contract is implicit, every team derives its own by trial and error, which always converges on heavier selectors and `!important`, and those are permanent: I can change my CSS, but I cannot un-escalate theirs.

code

css · 9 lines
css
@layer ds.reset, ds.base, ds.components, product, overrides;

@layer ds.components {
  :where(.ds-button) { padding: 0.5rem 1rem; }
}

@layer product {
  .checkout-cta { padding: 1rem 2rem; }
}

go deeper

for a junior

Understand that a shared system needs a stated place for overrides to live, and that guessing your way to one usually ends in heavier selectors and !important.

for a middle

Explain the mechanisms that make the contract real: a reserved layer for product code, zero-weight defaults, and custom properties for values meant to be configured.

for a senior

Show how you would enforce it — a linted ceiling, an allowlisted escape hatch, baselining for existing code — and name the boundary between overridable visuals and structural rules.

for a principal

Own the consequence you cannot undo: consumer escalation is permanent and turns your selectors into their API. Be ready to version the layer order and to read exception volume as system feedback.

## What an override contract actually is When a design system ships CSS, consumers will override it — that is not a failure mode, it is the normal life of a shared component. The only real question is whether the override path is designed or discovered. A contract makes it designed, and it has to answer three things explicitly: 1. **Where does a product override go**, such that it is guaranteed to win? 2. **What is guaranteed to be overridable**, and what is structural and should not be touched? 3. **What happens when neither answer fits** — the escape hatch, and who approves it? Everything else is mechanism in service of those three answers. ## The mechanism stack **A published layer order.** The system declares the order and reserves space for consumers: ```css @layer ds.reset, ds.base, ds.components, product, overrides; ``` That is the load-bearing artifact. It says: anything a product team writes in `product` beats every design-system declaration regardless of selector weight, which removes the only real reason to escalate. Because layer priority follows the order in which layer names are first encountered in the final bundle, the system must declare the full order up front, in a file guaranteed to load first — otherwise a consumer's bundler decides your precedence for you. Layers are available across the major browsers since 2022 (Chrome 99, Firefox 97, Safari 15.4). **Zero-weight defaults.** Rules the system expects to be replaced ship wrapped in `:where()`, so they lose to anything authored even if the layer order is somehow misconfigured. This is belt-and-braces, and it matters precisely because you do not control consumer builds. **Custom properties as the configuration surface.** For values you can predict people will change — spacing scale, radii, brand colour, density — a property read as `var(--ds-space-md, 1rem)` lets a team set a value on an ancestor and win by inheritance, with no cascade contest at all. This is the most important part of the contract in practice, because every case it absorbs is a case that never becomes a selector. **A named escape hatch.** There will be a vendor widget injecting inline styles, or a legal banner that must win. Give that a home — one file, in the top layer, allowlisted for `!important` — so the exception does not normalise itself across the codebase. ## Enforcement, or it is just documentation A contract nobody can violate accidentally is worth ten that rely on good intentions: - `selector-max-specificity` in stylelint, set to the flat ceiling, applied to product repositories - `declaration-no-important` everywhere except the allowlisted escape-hatch file - CI checks that product CSS is inside the expected layer - a review path for exceptions, with an expiry, so "temporary" is measurable The enforcement has to be adoptable incrementally: existing violations baselined, new code held to the ceiling. A rule that fails a hundred existing files on day one is switched off on day two. ## What breaks when it is implicit Without a stated contract, each team runs its own experiment. They try a class, it loses to a system rule, they add an ancestor, it wins, and that becomes their house style. The result compounds in three ways: - **Escalation is one-way and it is theirs, not yours.** You can lower your own selectors' weight in a release; you cannot lower theirs. Every heavy override written this quarter constrains what you can safely change next quarter. - **You lose the ability to ship changes.** Once consumers depend on beating specific system selectors, any change to your selector structure — a refactor that shortens a chain, a rename — silently changes who wins in their app. Your internals became their API. - **`!important` becomes the lingua franca.** It is the one move that reliably works without understanding anything, so in the absence of a sanctioned path it is what teams converge on. After that, even you cannot override your own system cleanly. ## The governance part The layer names and their order are public API. Renaming `ds.components` or inserting a layer in the middle of the order changes precedence in every consumer, so it is a major-version change and needs the same care as removing an exported function. Document the order, version it, and give consumers a migration note when it moves. The harder judgment is the boundary between "overridable" and "structural". Say it explicitly: visual defaults are yours to restyle, and the rules that make a component function — overlay stacking, focus behaviour, scroll containment, hit areas — are not. Then be honest that this line will be crossed, and have a route for it that ends in a system change rather than a private workaround, because a workaround you never see is a bug report you will get much later, at a much worse time.

  • Why treat the cascade layer order as a versioned public API rather than an internal detail?
    Because precedence in every consuming app depends on it. Renaming a layer or inserting one in the middle changes which declarations win across products that never changed a line, and the symptom is visual rather than a build failure. Publish the order, treat changes as breaking, and ship a migration note — the same discipline you would apply to removing an exported symbol.
  • A product team says the contract does not cover their case and they need !important today. How do you handle it?
    Grant it in the sanctioned place rather than refusing. One allowlisted file in the top layer, an owner, and an expiry date, so the exception is visible and countable. Then treat it as a signal: repeated exceptions in the same area usually mean a missing custom property or a component that is not configurable enough, and that is a system backlog item, not a discipline problem.
  • How do you know whether the contract is actually working?
    Measure the consumers, not the system. Track `!important` counts and selector weight in product repositories, how many overrides target design-system class names directly, and how many exceptions are open past their expiry. A rising count of heavy overrides on one component tells you exactly which part of the system is under-configured, well before anyone files a ticket about it.

saying these in an interview costs you the question

  • Relies on documentation alone with no linting or CI check
  • Assumes product teams will simply not override the system
  • Treats layer names as an internal detail free to rename
  • Bans !important without providing a sanctioned override path
  • Ships heavy system selectors and calls overrides a misuse

context