How much customisation should a shared component expose before it stops being a design system?
Design system component customization should expose sanctioned variants, slots for content and a visible escape hatch, not arbitrary styling on every part. Once any product can restyle a shared component freely, consistency becomes unenforceable.
What the interviewer is scoring
- Does the candidate distinguish sanctioned variants from arbitrary styling, rather than counting props
- Whether composition and slots are offered as the alternative to a growing options list
- That the answer treats an escape hatch as necessary, and says how its use is made visible
- Whether the boolean-prop explosion and its combinatorial testing cost get named
- Can they say what evidence would justify promoting one team's override into the system
Answer
Short answer
A shared design system component should expose only variations the design language supports, such as size, intent and state. Use slots or composition for content differences, keep one visible escape hatch for exceptions, and promote repeated overrides into sanctioned variants only when the pattern proves reusable.
Use design systems as the constraint on customisation: a component can offer variation, but it should not let each team recreate the brand, accessibility model, or interaction contract. Good design systems work makes the allowed choices explicit so local flexibility does not become a second component library.
The tension stated honestly
A design system exists to make the same intent look the same everywhere. A product team exists to ship the screen in front of them. Every request for one more prop is a real need from the second party that costs the first party a little of its reason to exist, and there is no configuration of the library that removes the conflict. What you can do is decide, deliberately, which axes of variation the design language sanctions, and make everything else either a composition problem or a visible exception.
The useful test for a proposed prop is whether a designer could point at a rule that governs it. variant="destructive" is sanctioned: there is a documented meaning, a token pairing behind it, and a right answer to which one you should use. borderRadius={9} is not: it encodes one screen's taste, and once it exists any value between 0 and 40 is equally legitimate, which means the component no longer expresses a decision.
What a defensible API looks like
prop type why it is allowed
----------- -------------------------------- -----------------------------------------
variant primary | secondary | destructive finite, documented, token-backed
size sm | md | lg matches the spacing scale, three steps
icon ReactNode slot: caller owns content, not styling
disabled boolean a state, not an appearance
fullWidth boolean layout affordance, no visual invention
asChild boolean renders as a link, keeps the styling
className string escape hatch, tracked, see below
The pattern in the allowed column is that each prop either selects from a closed set the design language defines, or hands over content while the component keeps appearance. The icon slot is the important one, because it is what you offer instead of iconName, iconPosition, iconColour and iconSize — four props, sixteen combinations to test, and a growing chance that some combination looks broken. Composition converts an options problem into a children problem, and children do not multiply your test matrix.
Boolean props are where libraries decay fastest. Two booleans are four states; six are sixty-four, most of which nobody has ever rendered and several of which are contradictory. When you notice that a combination has no meaning — compact with fullWidth, say — that is the signal that you had a closed set all along and modelled it as flags. Collapsing them into a single enum removes the impossible states from the type system rather than from the documentation.
Why the escape hatch has to exist
Refuse all overrides and you do not get consistency, you get a fork. The team with the deadline copies your component into their repository, deletes the parts they do not need, and now you have a permanent divergence you cannot patch, upgrade or audit. The escape hatch is what keeps that team inside the system where you can still see them.
So allow a styling override, and make its use legible. Concretely: keep a single sanctioned way in — one className or style-override prop, not a per-part styling object that lets a caller rewrite the internals — and put a lint rule on it that requires a comment, or a rule that permits it only in application code and never in another shared component. Then read what accumulates. Overrides are the highest-quality demand signal you will ever get, because each one is a team paying a cost to tell you something the system does not do.
flowchart TD
A["Team needs a variation"] --> B{"Does a design rule cover it?"}
B -- yes --> C["Add a sanctioned variant"]
B -- no --> D{"Is it content rather than style?"}
D -- yes --> E["Expose a slot"]
D -- no --> F["Override, logged"]
F --> G{"Third team asking?"}
G -- yes --> C
G -- no --> H["Stays a local exception"]The branch worth studying is the loop from the override back to a sanctioned variant. That is the mechanism by which a system learns, and a system without it either says no until teams fork or says yes until it is a styling library.
Where the flexibility argument goes wrong in an interview
Candidates usually pick a side and defend it, and both sides lose. "Lock it down, consistency matters" ignores that you have no authority over a product team's roadmap and that forks are the observed consequence. "Make it flexible, teams know their users" ignores that unconstrained flexibility means the fourteen buttons on a checkout page were each styled by whoever was on that ticket.
The answer that lands says the decision belongs to different layers. Primitives can be unstyled and infinitely composable because their job is behaviour — focus management, keyboard interaction, ARIA wiring — and behaviour is not what the brand is about. The styled layer on top of them should be opinionated to the point of being slightly annoying, because that is what makes it worth adopting. And the promotion path between them, with a threshold you state out loud, is what stops the argument recurring every sprint.
A shared component's job is to make the sanctioned choice the easy one, not the only one — and to leave a trace whenever someone needed something else.
© 2026 Preptima. Originally published at preptima.com.
Likely follow-ups
- A team needs a button with a spinner inside it. Do you add a loading prop or let them compose one?
- How would you measure whether your escape hatch is being used for legitimate cases or as a bypass?
- What is the argument for shipping unstyled primitives plus a theme rather than finished components?
- When two products need genuinely different visual languages, what belongs in the shared layer at all?
Related questions
- Two clients open the same record, both edit it, and the second save silently overwrites the first. How would you use ETags to turn that lost update into something the client can see and handle?mediumAlso on api-design4 min
- Every uploaded image needs six sizes and the thumbnail has to appear immediately. What runs before you return, and what does not?mediumAlso on api-design5 min
- How do you implement a scalable architecture and tooling to automatically enforce strict WCAG 2.1 AA compliance across dozens of autonomous frontend teams without crippling developer velocity?hardAlso on design-systems3 min
- The same domain has to be exposed to a mobile app, a partner integration and internal service-to-service traffic. Where does GraphQL fit, where does gRPC, and where does neither?hardAlso on api-design7 min