Design Systems and Component Libraries Questions
Building and scaling reusable design foundations: component architecture, design tokens, pattern libraries, versioning, governance, and adoption across teams. Covers ensuring visual and behavioral consistency, evolving a system without breaking consumers, and the tooling and cross-functional alignment that keep a design system healthy at scale.
You are asked to perform a manual visual consistency audit of an existing product that has grown organically for years with no design system oversight. Provide a step-by-step checklist for what you'd actually inspect, what sampling strategy you'd use across pages and devices, what tools you'd employ, and how you'd present and prioritize findings in a remediation report.
Sample Answer
Direct answer
Run the audit as a sampled, checklist-driven inspection across a representative slice of the product, not an exhaustive page-by-page sweep, and prioritize what you find by user-facing impact (does this block a task or fail accessibility) rather than by visual severity alone, because a legacy product with years of organic drift will surface far more findings than any team can fix at once.
Structured elaboration
What to inspect
| Category | Specific checks |
|---|---|
| Color | Primary/secondary/semantic token usage, contrast ratios, hover/focus/disabled states |
| Typography | Font family, size, line-height, weight, letter-spacing, responsive scaling |
| Spacing and layout | Grid alignment, gutters, margins, vertical rhythm, component padding |
| Component variants | Buttons, inputs, cards, modals: are the same states styled the same way everywhere |
| Iconography | Stroke width, size, alignment relative to adjacent text |
| Accessibility | Contrast against WCAG 2.1 AA, visible keyboard focus, minimum readable font sizes, touch target size |
| Imagery | Aspect ratio and crop consistency, placeholder/empty-image handling |
Sampling strategy
- Pages: the home/entry page, the top 2-3 highest-traffic workflows (signup, core task, checkout-equivalent), and explicitly error and empty states, which are the most commonly neglected surfaces in an organically-grown product.
- Devices and viewports: desktop, tablet, and mobile breakpoints for each sampled page, since drift is often breakpoint-specific (a component that looks fine at 1440px but breaks its own spacing rules at 375px).
- Depth over breadth: fully audit a smaller, representative set of pages rather than skimming everything, because a shallow full-product pass produces a findings list too large and too vague to act on.
Tools
- Design-side comparison: Figma, for checking live product screens against the intended token values.
- Automated checks: an accessibility scanner (axe or the browser's built-in Lighthouse audit) for contrast and semantic issues, run on every sampled page to catch what a human eye misses or under-counts.
- Manual contrast verification for any borderline cases the automated scanner flags as ambiguous.
- Documentation: a shared spreadsheet or tracker with annotated screenshots, one row per finding, so the remediation report is generated from real evidence rather than reconstructed from memory afterward.
Presenting and prioritizing findings
- Prioritization tiers: P0 for accessibility regressions or anything blocking task completion; P1 for token mismatches or spacing drift on the highest-traffic flows; P2 for variant drift on lower-traffic surfaces; P3 for cosmetic issues on rarely-visited pages.
- Each finding in the report includes a before screenshot, the specific token or spec it should match, and a severity tier, so an engineer can act on it without a follow-up meeting.
- The report closes with a small number of representative fixes proposed as a first remediation batch, not the full findings list, so the team has a concrete, scoped starting point instead of an intimidating backlog.
Worked example
Auditing three sampled pages (a signup form, a settings page, and a checkout-equivalent flow) at desktop and mobile breakpoints, the primary CTA button's hover state fails contrast against its background on two of the three pages, but not the third, where an older button variant happens to use a different, still-compliant color pair. That's a P0 finding (accessibility, on high-traffic flows) rather than a P2, even though visually it might look like a minor color variation, because it fails a WCAG contrast check on task-critical screens. A separate finding, inconsistent card corner radius between the settings page and the checkout flow, is real drift but doesn't block any task or fail any accessibility check, so it's logged as P2 and grouped into a later cleanup batch rather than the first remediation pass.
Trade-offs & pitfalls
Sampling introduces real bias risk: if the sampled pages happen to be the ones a recent redesign already touched, the audit will under-report the true scale of drift elsewhere in the product, so the sample should deliberately include pages nobody has touched recently, not just the highest-traffic ones. Automated tools catch contrast and some structural issues reliably but miss "these two buttons are the same color but semantically mean different things," which needs a human eye; relying on automation alone produces a report that looks complete but misses the drift that actually confuses users. The biggest presentation pitfall is handing over the entire findings list as one flat backlog: without the P0 through P3 triage and a proposed first batch, a legacy audit report reads as overwhelming and often gets shelved rather than acted on.
Your product is expanding into a market that reads right-to-left and uses a script with very different typographic needs than what your design system was built around. Design token strategies to support internationalization and localization for cases like this. Walk through what actually breaks when you localize a design system built for one language and region, and how your tokens would need to adapt. Explain how you'd test across locales and platforms to catch layout and visual regressions.
Sample Answer
Direct answer
Expanding into an RTL market with a very different script breaks three separate things: physical directional CSS (left/right assumptions), typography tuned for one script's metrics, and fixed sizing built around one language's average word length. Fix all three at the token layer: replace physical properties with logical ones (inline-start/end, block-start/end), make typography tokens parameterized by script, and express sizing as minimums rather than fixed values so text length can vary safely.
Structured elaboration
What actually breaks
- Layout: left/right padding, margin, text-align, and icon/element ordering are all direction-dependent and silently wrong once mirrored.
- Typography: line-height and vertical rhythm tuned for Latin script clips Arabic diacritics or misaligns CJK glyphs; font stacks built for Latin lack glyph coverage for the new script entirely.
- Sizing: buttons and inputs sized to fit English labels truncate longer translations (German averages meaningfully longer word length) or clip Arabic; fixed-width components need to become minimum-width components.
- Iconography and color: directional icons (chevrons meaning "next") point the wrong way once mirrored; some colors carry different meaning by locale and should not be assumed universal.
- Mixed-direction content: numerals embedded in RTL text and bidi punctuation need explicit handling, not just a blanket
dir="rtl"on the page.
Token strategy
| Concern | Token approach |
|---|---|
| Layout direction | spacing.inline-start/inline-end, spacing.block-start/block-end instead of left/right/top/bottom - the browser resolves inline-start to left in LTR and right in RTL automatically |
| Typography per script | line-height.body = { latin: 1.5, arabic: 1.8, cjk: 1.7 }; font-family tokens keyed by script with fallback chains |
| Icon mirroring | icon.mirror = true/false per icon - directional icons (next/back) mirror; non-directional icons (logo, a clock) do not |
| Sizing | button.min-inline-size instead of a fixed width, so labels can grow without clipping |
| Color/meaning | A locale-color-mapping table only where meaning genuinely differs, documented explicitly rather than silently swapped |
Worked example
Take a "Save changes" button. English label is 12 characters. The German equivalent, "Änderungen speichern," is 20 characters - roughly 1.67x the English character count (20 / 12 ≈ 1.67). If the button token were a fixed width (say, 96px), the German label would clip. Using button.min-inline-size: 96px with padding-inline: 16px on each side and width driven by content instead of a fixed value, the button grows to fit 21 characters instead of truncating them. This isn't a claim about exact pixel rendering (font metrics vary by typeface, which isn't being asserted here) - the number that matters is the character-count ratio itself, and it's exactly why a fixed-width token structurally cannot absorb translation length variance while a minimum-width token can.
Testing across locales and platforms
- Automated: render every component for each supported locale x direction combination through a visual-regression tool, diffed against an approved per-locale baseline; a DOM-level overflow check (
scrollWidth > clientWidth) fails the build automatically on any truncation, catching it before a human has to review 12 locales by eye. - Include a pseudo-locale in CI (strings algorithmically stretched ~40% and direction-reversed) so length and mirroring regressions surface even before real translations exist.
- Manual: native-locale reviewers specifically check color meaning and reading flow, since automated tools can verify layout but not cultural appropriateness.
Trade-offs & pitfalls
- Logical CSS properties are near-universally supported in browsers, but native platforms don't have inline-start/end as a first-class concept - it requires an explicit direction-aware mapping step in the native token build, which is real added tooling investment.
- Per-script typography tokens add genuine maintenance surface. The alternative (one line-height for everything) is simpler but clips non-Latin scripts - this is a case where correctness costs complexity, and skipping it only works if the product genuinely never ships to that script.
- The most common wrong turn is mirroring every icon with a blanket
transform: scaleX(-1)- this flips non-directional icons too (a play button, a person avatar) and looks visibly broken. Mirroring must be an explicit per-icon token decision. - Fixed min-width buttons are a frequent, easy-to-miss localization break. Treat "does this component assume one language's average word length" as an explicit design-review checklist item rather than something QA discovers after translation.
Explain semantic versioning (semver) for UI components and a shared component library. Provide concrete examples: (a) a bug fix that should be a patch, (b) a new non-breaking feature that should be a minor bump, and (c) a breaking API change that should be a major version. Describe how you would communicate these different release types to downstream teams and tools you would use for changelogs.
Sample Answer
Direct answer
Semantic versioning gives a component library's version number, MAJOR.MINOR.PATCH, a contract meaning: PATCH is a backwards-compatible bug fix, MINOR is a backwards-compatible addition, and MAJOR is a change that can break a consumer. The point is that a consumer can safely auto-update on patches and minors (^1.4.2 in npm terms) without reading a changelog, and knows a major bump means "stop and read before upgrading."
Structured elaboration
What changes at each level
| Bump | Meaning | Consumer impact | Example trigger |
|---|---|---|---|
| PATCH | Backwards-compatible bug fix | Safe to auto-update, no code changes needed | Fixing a missing aria-label on an icon-only button |
| MINOR | Backwards-compatible addition | Safe to auto-update, new capability is opt-in | Adding a new size="xxs" prop or a new Badge component |
| MAJOR | Breaking change to public API or rendered output | Requires the consumer to read the migration guide and update code | Renaming <Button variant="primary"> to <Button tone="brand"> |
The test for "does this need a major bump" is not "did the code change a lot," it is "does any existing correct usage now behave differently or fail to compile/render." A large refactor that keeps the public API and visuals identical is still a patch.
Communicating each release type
- Changelog: generate it from commit messages using a convention (Conventional Commits:
fix:,feat:,feat!:orBREAKING CHANGE:footer) so the bump level and the changelog entry come from the same source of truth instead of being decided twice. - Release notes for majors: always include a migration guide with before/after code snippets, not just a prose description of what changed.
- Distribution channels: GitHub Releases or an internal package registry page for the human-readable notes, plus an in-repo
CHANGELOG.mdso it is visible in the diff of any PR that bumps the dependency.
Tooling
- Automated bump and changelog:
semantic-releaseorchangesetsread Conventional Commits and produce the version bump, changelog, and tag automatically, removing the "did I mean minor or patch" judgment call from a human at release time. - Breaking-change detection assist: a type-diff check (for example comparing the public
.d.tsAPI surface between versions) catches accidental breaking changes that were not intentionally flagged withfeat!:, which is a common source of "silent major" bugs.
Worked example
A library is at 1.4.2. Three independent changes land:
- (a) Patch, 1.4.2 -> 1.4.3:
Button's icon-only variant is missing anaria-label, so screen readers announce nothing. The fix adds the label. The prop API, markup structure, and visual layout are unchanged for every existing usage, so this is a bug fix with zero consumer action required. - (b) Minor, 1.4.3 -> 1.5.0: A new
Badgecomponent is added, andButtongains an optionalsize="xxs"prop that defaults to the previous size when omitted. Every existing usage ofButtoncompiles and renders identically; only code that opts in to the new prop is affected. This is additive, so it is a minor bump, not a patch, because it is new surface area (even though nothing breaks). - (c) Major, 1.5.0 -> 2.0.0:
Button'svariantprop is renamed totone, and the values change from"primary" | "secondary"to"brand" | "neutral". Any component currently written as<Button variant="primary">now either fails to compile (in TypeScript) or silently renders with no styling (in plain JS, sincevariantis simply ignored). That is a breaking change to every consumer using the old prop, so it is a major bump regardless of how small the code diff looks.
Trade-offs and pitfalls
- Shipping a behavior change "as a patch" because it feels small is the most common semver violation; if any correctly-written existing usage now behaves differently, it is not a patch, no matter how minor it looks to the author.
- Renaming or removing a prop without a deprecation window forces every consumer to upgrade and migrate simultaneously; a senior answer proposes keeping the old prop working (with a console deprecation warning) for at least one major cycle, or shipping a codemod (an automated script that mechanically rewrites consumer code from the old API shape to the new one, so teams don't hand-edit every call site), rather than a hard cutover.
- Auto-generated changelogs from commit messages are only trustworthy if the team actually follows the commit convention; a mislabeled
fix:on a change that is actually breaking produces a version that lies about its own risk, which is worse than a hand-written changelog because consumers trust the automation. - Treating "internal refactor with no API change" as needing any bump at all is unnecessary churn; reserve version bumps for changes visible to a consumer.
Some visual properties are context-dependent (e.g., card elevation vs modal elevation). How would you model contextual tokens without exploding the token namespace? Propose an approach that supports context composition and keeps tokens maintainable.
Sample Answer
Direct answer
Model context-dependent values as a composition of a small set of intent-based base tokens (surface, overlay) and a small set of context modifiers (card, modal), resolved together at build or render time, rather than creating a separate token for every property-by-context combination. The namespace stays small because contexts are reusable modifiers, not one-off tokens.
Structured elaboration
Why naive per-combination tokens explode
If elevation alone needs a distinct token for every host component (card, modal, dropdown, tooltip, popover), and the system has, say, four elevation levels and six contexts, that's already 24 tokens for one property, and every new context multiplies the count further. The combination is the problem, not the base values.
Two-layer model
- Base semantic tokens: express intent, not context.
elevation.surface(a subtle resting shadow) andelevation.overlay(a strong shadow for content that floats above everything else) are the only two base values most systems actually need. - Context modifiers: a small, named set of scale/offset adjustments.
context.cardmight dampen the base shadow (it's a resting element),context.modalmight amplify it (it needs to visually separate from the whole page). - Composition: a resolver combines base token and modifier at the point of use, rather than a designer or engineer having to remember a bespoke value per context.
Precedence for stacked contexts
When a card renders inside a modal, apply modifiers in a documented, fixed order (innermost first) rather than an ad hoc combination, so a component author never has to guess which modifier wins.
Worked example
Base tokens:
:root {
--elevation-surface-y: 2px;
--elevation-surface-blur: 8px;
--elevation-surface-alpha: 0.12;
--context-card-scale: 0.8;
}
Composition:
.card {
--elev-y: calc(var(--elevation-surface-y) * var(--context-card-scale));
box-shadow: 0 var(--elev-y) var(--elevation-surface-blur) rgba(0, 0, 0, var(--elevation-surface-alpha));
}
The resolved value:
elevation.card.y=elevation.surface.y×context.card.scale=2px×0.8=1.6pxNo card-elevation token was ever authored, it's the composition of one base token and one reusable modifier, and the same context.card modifier applies to any other property (border-radius, opacity) a card variant needs to dampen, without a new token per property.
Trade-offs & pitfalls
Composition adds a layer of indirection: reading .card's final box-shadow value now requires resolving two tokens instead of reading one, which is a real cost for a component author debugging a visual issue without tooling support (a resolver preview in Figma or a browser devtools helper mitigates this, but only if someone builds it). The model also breaks down if teams start inventing new contexts ad hoc instead of reusing the documented set, at that point the namespace explosion just moved from properties to contexts. The right calibration is to keep the context list intentionally small and centrally owned, adding a new context should require the same review a new base token would.
Explore the trade-offs between strict constraint and opinionated systems (limited variants, prescriptive patterns) versus flexible systems (low-level primitives, escape hatches). Provide decision criteria you would use when making a choice, and propose concrete guardrails, review workflows, and technical patterns (e.g., opt-in composition) to manage exceptions while preserving consistency.
Sample Answer
Direct answer
Opinionated, constrained systems trade flexibility for consistency, speed, and lower cognitive load; flexible, primitive-based systems trade some consistency for the ability to handle edge cases the system's author never anticipated. The senior move is not to pick one system-wide, but to default opinionated for the common path and provide a small number of well-governed, opt-in escape hatches for the long tail, rather than making the whole system either rigid or wide open.
Structured elaboration
Decision criteria
| Dimension | Favors opinionated (strict) | Favors flexible (primitives) |
|---|---|---|
| Frequency of use | High-traffic, repeated flows (nav, forms, checkout) | Rare, one-off surfaces (a single campaign page) |
| Consistency stakes | Cross-product brand or legal/compliance surfaces | Isolated surfaces with no cross-product visibility |
| Team maturity | Newer or rotating teams who benefit from guardrails | Experienced teams who can be trusted with primitives |
| Variation across the org | Single product, single brand | Multiple brands, white-label, or highly divergent product lines |
| Time pressure vs. governance cost | Ship fast on the well-trodden path | A genuinely novel interaction with no existing pattern |
Guardrails
- The token layer (color, spacing, type scale) stays immutable outside an RFC. Tokens are the highest-leverage place for consistency, so they get the least flexibility, not the most.
- A two-tier component catalog: a small "core" tier that is opinionated by default, and a clearly labeled "extension" or "primitive" tier that trades guardrails for control.
- Automated enforcement: a design-token lint step in CI that flags hardcoded colors/spacing outside the token set, and a deprecation lint that flags usage of primitives that have graduated into a core pattern.
Review workflows
- New-pattern proposals go through a lightweight RFC: problem, why the existing opinionated component doesn't cover it, proposed primitive-level solution, and an explicit review from design + engineering + accessibility.
- A quarterly triage reviews everything built on primitives/escape hatches and either promotes recurring patterns into the core opinionated set or flags them for removal.
Technical pattern: opt-in composition
The default import of a component stays opinionated (fixed variants, no arbitrary style props). An explicit, differently-named import exposes the unstyled primitive for the rare case that needs it, so reaching for flexibility is a visible, deliberate choice rather than an accident:
// Opinionated default: limited, documented variants only
import { Button } from "@ds/button";
<Button variant="primary">Save</Button>
// Opt-in escape hatch: same behavior/a11y wiring, no style opinions
import { ButtonPrimitive } from "@ds/button/primitive";
<ButtonPrimitive className={campaignSpecificStyles}>Save</ButtonPrimitive>
Both share the same underlying keyboard/focus/ARIA logic (via a shared hook), so the flexible path never loses accessibility correctness, only visual opinion.
Worked example
A marketing team asks for a hero CTA button with a custom gradient and shape for a single campaign landing page. Applying the criteria: frequency is one-off (low), the surface is isolated to one campaign page rather than shared product chrome (low consistency stakes), and there's real time pressure. Conclusion: use the opt-in ButtonPrimitive escape hatch rather than modifying the core Button, tag the usage as an exception (see the exception-workflow pattern for governance), and don't touch the token layer. If three more teams request the same gradient treatment within the following quarter, that recurrence is the signal to promote it into a documented Button variant instead of leaving four independent one-offs in the wild.
Trade-offs & pitfalls
Too many guardrails without an escape hatch pushes teams into shadow systems: arbitrary inline styles that bypass the design system entirely, which is worse than a governed exception. Too few guardrails, especially at the token layer, causes visible drift and accessibility regressions that are expensive to unwind once dozens of products depend on them. The most common wrong turn is treating "flexible" as "unreviewed": an exception without an owner, an expiry, and a promotion/removal decision doesn't stay an exception, it quietly becomes permanent technical and design debt.
Unlock Full Question Bank
Get access to all Design Systems and Component Libraries interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.