skip to content

A charity site's team restyled a shared library's amount picker by targeting its internal element names; after a minor library release the restyle vanished. What went wrong, and how should the library define override points?

level: seniorimportance: should knowfreq 52%

answer

  1. public versus private styling surface
  2. internals were never promised
  3. tokens, variants, named parts
  4. repeated reach-ins signal a gap
  5. too few hooks versus frozen internals

basics

~20 s

The team depended on private structure the library never promised to keep. The library should publish a small, documented override surface — theme tokens, component override tokens, variants and a few named parts — and keep everything else private.

solid answer

~50 s

The consumer coupled its restyle to the amount picker's **private** structure — internal element names and nesting — which the library was free to refactor, so a minor release that wrapped the chip label and renamed an internal element silently stopped the rule from matching. Nothing errored; the campaign page just lost its look, and a half-applied restyle could even have left light text on a light chip. The fix is an explicit **override contract**: semantic tokens through the theme, component-scoped override tokens with semantic defaults, designed variants, and a short list of named, documented parts for structural tweaks, plus at most one clearly labelled escape hatch. Everything else is private and made hard to address. When several teams reach into the same internal, treat it as a missing token or variant and add one. The balance is deliberate: too few hooks push teams to reach in, too many freeze the internals.

go deeper

for a junior

Recall the sanctioned levers — theme tokens, component override tokens, variants — and that internal element names are not something to style against.

for a middle

Explain the public and private styling surfaces, why a reach-in fails silently rather than loudly, and how named parts differ from internals.

for a senior

Show how you would diagnose the silent regression, move consumers from reach-ins to hooks, and use repeated reach-ins as evidence for new tokens or variants.

for a principal

Set the policy for how large the override surface is allowed to grow, balancing consumer flexibility against the library's freedom to refactor and fix.

## What actually happened A charity donation site uses the library's **amount picker** — a group of preset amount chips. The campaign team wanted the selected chip's background to use the campaign accent, found no sanctioned way to do it, and wrote a rule that targeted the chip's internal element by its generated name and nesting. It worked. The library then shipped a minor release: to fix label alignment, it wrapped the chip's label in an extra element and renamed an internal element. Under its own contract that was an internal refactor. The campaign team's rule no longer matched anything. There was no error and no warning — the selected chip simply went back to the default look. A worse outcome was equally possible: if the rule had changed the text color on one element and the background on another, only one half might have survived, leaving light text on a light chip. The root cause is not the release. It is that the consumer depended on something the library had never promised, and the library had given it no better option. ## Public and private styling surface A library component has two styling surfaces, and the library must say where the line is: | Layer | Example | Stability promise | |---|---|---| | Semantic tokens via the theme | `surface.selected` | system-wide contract | | Component override tokens | `amount-chip.surface-selected` | documented, stable | | Variants | size, emphasis | documented, stable | | Named parts | a documented chip and label part | stable, limited to the listed parts | | Escape hatch | extra style on the component's root | supported, no promise about internals | | Internal structure | nesting, generated names, element order | private, may change in any release | The top five rows are the **override contract**. The last row is **implementation**, and consumers who target it are writing against something that can change without notice. ## How the library should define override points 1. **Decide, per component, what consumers legitimately vary.** Surfaces, borders, radius, padding and icon placement are common; the component's state logic and focus indicator usually are not. 2. **Expose those as override tokens with semantic defaults,** so untouched instances still follow the theme and overridden ones change locally. 3. **Name a small set of parts** for structural adjustments that tokens cannot express, and document each one as public. 4. **Make the rest hard to address.** Generated or scoped internal names, the platform's encapsulation where it exists, and documentation that says plainly which names are private. 5. **Document the surface on every component page:** the tokens, variants and parts, each with a default and an example. 6. **Provide a fast path for gaps.** A request that becomes a token or a variant in the next release is cheaper for everyone than a reach-in. ## Finding and retiring reach-ins - A consumer-side **lint rule** can flag styles that target library internals. - Before refactoring internals, the library team can **search consuming codebases** for the names it plans to change. - **Repeated reach-ins are data.** When three teams override the same internal padding, the library is missing a hook; adding it turns three fragile rules into one supported one. - **The failure is silent,** so upgrades need a look at real consumer pages; no error or warning will report a rule that simply stopped matching. - Whether removing or renaming a *public* hook needs a major release is a separate classification question; the point here is that internals were never public. ## The trade-off - **Too few hooks** and consumers reach in or fork, and every internal refactor breaks someone invisibly. - **Too many hooks** and the component's internal structure is public under another name; no refactor is safe. - The workable middle is a **small, deliberate surface** that grows from evidence of real, recurring needs. - **A signal worth watching** is escape-hatch use per component: a rising count means the public surface is lagging behind what consumers actually need. ## On native mobile The same failure appears as subclassing a library view and walking its subview hierarchy, or relying on the order of internal views. It breaks for the same reason and is fixed the same way: theme properties, component style parameters and a few documented sub-elements are public; the view tree inside is not.

  • Three teams override the same internal padding on the amount chip. What should the library do?
    Treat it as evidence of a missing hook. Ask what the teams are trying to achieve; if it is a recurring, legitimate adjustment, expose a component override token for that padding with a semantic default, or a designed size variant if the need is a distinct look. Then help the teams move to it and flag the old reach-in with a consumer lint rule.
  • Why not simply document every internal element name so reaching in becomes supported?
    Documenting every internal publishes the whole structure as contract, so no refactor — an alignment fix, an accessibility repair — can ship without breaking someone. It trades a clear, small surface for an unbounded one. A few named parts for real structural needs give the flexibility without freezing everything.
  • How is this different on native mobile?
    Only in spelling. Native consumers reach in by subclassing a library view and walking its subviews or relying on their order. The fix is the same contract: theme properties, component style parameters and a few documented sub-elements are public; the internal view tree is not.

saying these in an interview costs you the question

  • The library broke the consumer, so the refactor should have been reverted.
  • Any element a consumer can reach is part of the library's contract.
  • The safest library exposes an override for every internal element.
  • Reaching into internals is fine if the team pins the library version.
  • Consumer reach-ins are a consumer problem the library can ignore.