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.
A pull request adds a brand-new component to the design system and is requesting a merge into main. Before you approve it, what would you check? Walk through the full set of QA acceptance criteria you'd want satisfied before this ships, and explain why each one matters enough to be a gate rather than a nice-to-have.
Sample Answer
Direct answer
Gate on the things that are expensive or impossible to fix after teams have already adopted the component, accessibility, public API shape, and visual-regression baseline, and treat purely cosmetic nits as non-blocking follow-ups. The test for "is this a gate or a nice-to-have" is simple: would fixing it after ten teams have imported this component be far more expensive than fixing it now?
Structured elaboration
| Check | Why it's a gate, not a nice-to-have | How enforced |
|---|---|---|
| Accessibility (automated + manual) | Retrofitting accessibility across every consumer once a component ships is far more expensive than fixing it before merge, and a11y regressions are often invisible until a real user or audit hits them | Automated: axe/eslint-plugin-jsx-a11y in CI. Manual: a scripted keyboard-only pass and one screen reader spot check |
| Public API design (prop naming, controlled/uncontrolled support) | The exported API is a breaking-change surface the moment it's published; renaming a prop later means a coordinated migration across every consumer | Manual design review against the system's naming and API conventions |
| Visual regression baseline | Without a baseline, silent visual drift in any state (hover, disabled, error) goes undetected until a user reports it | Automated: snapshot/visual-diff tool run in CI, baseline captured on merge |
| Documentation: a Storybook story per variant and state | Undocumented components get reimplemented ad hoc elsewhere because nobody can discover what already exists, defeating the point of a shared system | Manual: PR checklist item, CI can verify a story file exists per exported variant |
| Token-only styling (no hardcoded colors/spacing) | This is the property that keeps "a design system" different from "a folder of similar-looking components"; one hardcoded hex value is how drift starts | Automated: a design-token lint rule |
| Bundle-size delta | A size increase in a shared component compounds across every app that imports it, unlike a one-off feature | Automated: bundle-size diff check in CI with a stated budget |
Worked example
A PR adds a new Tooltip component. Walking it through the checklist: it's missing focus-trap and Escape-key handling for the tooltip's interactive content, that blocks merge, since retrofitting keyboard behavior after adoption means auditing every page that already uses it. It's also missing a Storybook story for the "long content, wraps to multiple lines" state, that also blocks merge, since that's exactly the case a future consumer will hit and have no documented answer for. A minor 2px spacing inconsistency between the arrow and the tooltip body is filed as a non-blocking follow-up ticket rather than blocking the PR, since it's cheap to fix later and doesn't compound the way the other two issues would.
Trade-offs & pitfalls
A checklist that gates on everything slows every PR down and, past a certain point, gets rubber-stamped once reviewers feel checklist fatigue, which defeats the purpose entirely. The fix is automating every objective check (axe, bundle-size diff, visual regression, token lint) so a human reviewer only has to make the judgment calls: API shape and documentation completeness. Keep the truly launch-blocking list short, around five items that map to genuinely expensive-to-reverse problems, and treat everything else as recommended-but-waivable with a follow-up ticket, rather than trying to gate on every possible improvement in one PR.
Advocacy and training: half the product teams at your company still aren't using the design system and adoption has stalled. Describe an evangelism plan to change that. Include the specific activities you'd run, incentives or KPIs for teams to adopt, documentation improvements, and how you'd measure whether each activity is actually working.
Sample Answer
Before running any activity, find out why half the teams haven't adopted, a technical gap (a needed variant doesn't exist), an awareness gap (they don't know the system covers their case), or a trust gap (past releases were slow or buggy), because the right evangelism plan differs by cause. The plan itself has two layers: an org-wide campaign to drive the initial push, and a steady-state onboarding and maintenance ritual so adoption doesn't regress once the push ends.
Diagnose first
A short audit across the non-adopting teams: sample their screens against the system's components, and a quick survey (or a handful of 1:1s) asking directly what's blocking them. This turns "half the teams haven't adopted" from one problem into a small number of concrete blockers to address.
Org-wide campaign activities
- Roadshow demos: short, recorded walkthroughs per squad showing real components and tokens relevant to their product, not a generic system overview.
- Champion program: 1 to 2 reps per non-adopting team, given early access to upcoming changes, a dedicated channel, and quarterly syncs; champions are the on-the-ground advocate who make the system's case in their own team's standups.
- Structured onboarding curriculum: a defined 90-day milestone structure rather than a one-off workshop, for example: weeks 1 to 2 audit the team's current UI against the system, weeks 3 to 6 migrate the team's top three screens, weeks 7 to 12 reach full fluency and contribute one pattern back upstream.
- Individual onboarding: for a single new designer/engineer pair joining a team, a concrete 30-day plan (week 1: read docs, build one component from the library into a real screen; week 2 to 3: pair with a champion on a migration; week 4: contribute a small doc fix or pattern back), with short weekly check-ins.
PM-specific lever: tie adoption to the roadmap
Adoption stalls when design-system work is always deprioritized against feature work. The fix a PM can drive is making adoption a scheduled part of the roadmap, not a favor: a phased rollout plan with adoption gates tied to release milestones (for example, "screen X ships only once it's built on system components") turns adoption from optional cleanup into a release requirement.
Upstream-contribution lever
To stop teams silently forking product-specific variants instead of contributing back, make contributing back genuinely easier than forking: a lightweight RFC template, a fast review SLA (target under one week for a small variant proposal), and visible credit for merged contributions in release notes.
Sustaining adoption after the push
A weekly design-engineering sync, a token/version sync cadence so consuming teams aren't surprised by drift, and a component sign-off review gate before a new pattern ships, keep the system healthy after the initial campaign ends; without an ongoing ritual, adoption regresses back toward zero as the campaign's energy fades.
Handling a resistant, high-impact team
Listen first: find the actual blocker (a missing variant, a performance concern, a release-timeline conflict) rather than assuming it's simple resistance. Propose a small, time-boxed pilot on one screen to de-risk the ask. Negotiate a compromise where possible (they keep one custom variant but agree to consume shared tokens, so at minimum visual consistency holds even if component reuse doesn't). Escalate to a sponsor only if the team's fork creates real brand or accessibility risk, not simply because they said no once.
Worked example: setting a measurable target
If the audit finds 5 of 10 product teams are on the system today (the "half" in the prompt, 5/10=0.5), a realistic two-quarter target from a champion-led rollout would be 8 of 10 teams (8/10=0.8), tracked by the KPI "% of UI surface built from system components" per team, not by roadshow attendance, which measures interest, not adoption.
Trade-offs and pitfalls
Chasing engagement metrics like roadshow attendance or Slack channel size instead of usage metrics (component adoption in shipped code, migration completion rate) makes a stalled campaign look successful right up until someone checks the actual UI. Mandating adoption top-down without first fixing the real blocker just pushes teams toward quieter forking instead of open resistance, which is harder to detect and fix later. And a champion program with no real incentive (early access, credit, a say in the roadmap) fizzles after the initial enthusiasm, so the steady-state ritual matters as much as the launch campaign.
Design a robust component API for a DatePicker that supports both controlled and uncontrolled usage, partial props (e.g., minDate only), internationalization, and future extensions without breaking consumer code. Provide an example of the public API (props and events), explain internal state handling, and outline a deprecation path if a default behavior must change later.
Sample Answer
Direct answer
Design the DatePicker around three principles that make it safe to extend later: value ownership is resolved once via a controlled/uncontrolled check, every constraint (min date, max date, disabled dates) is optional and applies independently so partial configuration works without requiring the others, and any future breaking change to a default ships behind an opt-in flag with a defined deprecation window rather than silently changing behavior under existing consumers.
Structured elaboration
Public API
interface DatePickerProps {
// Value ownership
value?: Date | null; // controlled
defaultValue?: Date | null; // uncontrolled
onChange?: (next: Date | null, meta: { source: "user" | "programmatic" }) => void;
// Independent, optional constraints
minDate?: Date;
maxDate?: Date;
disabledDates?: (d: Date) => boolean;
// i18n / presentation
locale?: string; // e.g. "en-US", defaults to browser locale
dateFormat?: string; // Intl-compatible format token
firstDayOfWeek?: number;
// Accessibility & customization
"aria-label"?: string;
renderDay?: (d: Date, state: { selected: boolean; disabled: boolean }) => React.ReactNode;
// Forward-compatible extension point
optInto?: ("weekNumbersV2" | "localeFirstDayOfWeek")[];
}
Internal state handling
Resolve ownership once with const isControlled = value !== undefined. In controlled mode, the internal display state (which month is currently shown) still lives locally, but the selected value itself is never mutated internally - every interaction calls onChange and waits for the parent to feed the new value back through value. In uncontrolled mode the component owns both.
Partial props
minDate alone, with no maxDate, just clamps one side. Validity is computed with each constraint independent: isValid = (!minDate || d >= minDate) && (!maxDate || d <= maxDate) && !disabledDates?.(d), so a consumer specifying only one constraint never has to reason about the others.
i18n
locale and dateFormat drive Intl.DateTimeFormat for parsing and display; day/month labels and firstDayOfWeek derive from locale unless explicitly overridden, so a Spanish-locale consumer gets Monday-first weeks without extra configuration.
Accessibility
The calendar grid uses role="grid"/gridcell; arrow keys move focus by day, Home/End jump to week start/end, PageUp/PageDown change month, Escape closes. aria-label is required on the trigger when no visible label text is present. disabledDates entries get aria-disabled and are removed from the tab sequence, not just visually dimmed.
Worked example
Deprecation path for a future breaking default change - say, moving the default firstDayOfWeek from "always Sunday" to "locale-derived":
- Ship the new behavior behind
optInto: ["localeFirstDayOfWeek"]. The old default (Sunday) stays the default - zero impact on existing consumers. - Add a dev-only console warning when a consumer relies on the default in a non-Sunday-first-week locale without having opted in - surfaces the coming change without breaking anything yet.
- After a fixed window (a concrete, communicated number of minor releases), flip the default to locale-derived. Consumers who explicitly set
firstDayOfWeekor opted in/out keep their chosen behavior; only consumers who did nothing and were silently affected get the new default - and they're exactly the ones who already saw the warning in step 2. - Publish a migration guide, and since this specific change is mechanical (add the opt-in flag or explicitly set
firstDayOfWeek), ship a codemod alongside it. - Remove the flag and warning machinery one major version after the flip.
Trade-offs & pitfalls
- The
optIntoextension point uses a union of known flag names rather than a barestring[], deliberately - an untyped string array would compile with any typo and silently do nothing, which defeats the purpose of an opt-in mechanism. - Fully independent constraint props (
minDate,maxDate,disabledDates) are ergonomic but can combine into a state where every date in a visible month is disabled with no explanation to the user. The component should surface an explicit empty-state message when the visible range has zero selectable dates, not render a grid of disabled cells silently. - The common wrong turn is treating
value: nullandvalue: undefinedas equivalent.undefinedsignals uncontrolled mode (per theisControlledcheck above);nullin controlled mode is a legitimate "no date selected" value. Conflating the two either breaks controlled/uncontrolled detection or makes "clear the date" impossible for a controlled consumer to express.
As a UI Designer, you are asked to explain design tokens to a non-design stakeholder who will fund a cross-platform design system. Define what design tokens are, why they matter for visual consistency across platforms, and give a few concrete token examples of your choosing with sample values and a short explanation of how each maps to CSS variables and native platform tokens (iOS/Android).
Sample Answer
Direct answer
Design tokens are the smallest, named building blocks of a product's visual language, colors, spacing, type sizes, radii, that are defined once and translated automatically into whatever format each platform needs. Funding a token system means every future product, on any platform, inherits the same visual language automatically instead of every team re-implementing it by hand, which is both a consistency guarantee and a maintenance-cost reduction.
Structured elaboration
Why this matters to someone funding it, not just to designers
- Consistency at scale: without tokens, "our brand blue" exists as a dozen slightly different hex values scattered across web, iOS, and Android code, each one drifting a little further from the others every time someone eyeballs a color instead of looking it up.
- Speed of change: a rebrand or accessibility-driven contrast fix is a one-line change to the token source instead of a multi-week hunt through every codebase for hardcoded values.
- Cross-team leverage: designers, engineers, and PMs use the same name ("color.background.primary") in conversation, in Figma, and in code, which removes an entire category of handoff miscommunication.
How a token becomes a real thing on each platform
A token is defined once, then a build step (not a person) translates it into the format each platform already understands, so no platform team has to manually keep their copy in sync.
Worked example
Three concrete tokens and what they become on each platform:
-
Color token:
color.foreground=#1F2937(a dark slate gray)- Web:
--color-foreground: #1F2937; - iOS: a named color asset resolving to
UIColor(red: 0.12, green: 0.16, blue: 0.22, alpha: 1) - Android:
<color name="color_foreground">#1F2937</color>
- Web:
-
Spacing token:
spacing.stack.16=16px- Web:
--spacing-stack-16: 16px;(or1rem, since16px \div 16 = 1rem) - iOS:
let spacingStack16: CGFloat = 16 - Android:
<dimen name="spacing_stack_16">16dp</dimen>
- Web:
-
Typography token:
type.h1.size=32px,type.h1.weight=700- Web:
--type-h1-size: 32px; --type-h1-weight: 700; - iOS:
UIFont.systemFont(ofSize: 32, weight: .bold) - Android:
android:textSize="32sp"(note: text usessp, notdp, on Android specifically becausespscales with the user's accessibility font-size setting)
- Web:
Change the hex value once in the source, and all six platform artifacts above regenerate automatically on the next build, that's the pitch: one edit, zero manual re-implementation.
Trade-offs & pitfalls
The calibration risk in this conversation is going too deep into build-pipeline mechanics with a stakeholder who only needs the business case: consistency, speed of change, and reduced cross-platform drift are the three points that justify funding, the Style Dictionary-style tooling underneath is an implementation detail worth one sentence, not ten. The second pitfall is presenting tokens as a purely visual concern, the stronger argument for a non-design stakeholder is the maintenance-cost angle: without tokens, every visual fix is N times more expensive, where N is the number of platforms and products that hardcoded the value independently.
Your company just acquired four smaller brands, and leadership wants all five brands running on one shared component library while each keeps its own visual identity. Architect a token system and build pipeline that supports this: shared tokens where it makes sense, brand-specific overrides where it doesn't, and as little duplication and developer friction as possible. Walk through how tokens flow from source to the final published packages each brand consumes.
Sample Answer
Direct answer
Layer the token source in three tiers, core tokens shared by all five brands, a brand-override layer that only specifies what differs, and a component-override layer for the rare cases where a brand needs to diverge on one specific component rather than a whole token. Run all three tiers through one build pipeline that emits a versioned package per brand, so shared updates propagate everywhere automatically and brand-specific changes stay isolated.
Structured elaboration
flowchart LR
Core[Core tokens] --> Merge[Brand override layer]
Merge --> CompOverride[Component override layer]
CompOverride --> SD[Style Dictionary transform]
SD --> Web[Web CSS variables]
SD --> IOS[iOS Swift constants]
SD --> AND[Android resources]
Web --> PkgA[npm brand-a package]
Web --> PkgB[npm brand-b package]
Three tiers
- Core tokens (
core.json): spacing scale, grid, and semantic tokens that every brand shares (color.surface.primary,spacing.stack.16). This is the bulk of the token set, most values genuinely don't need to vary by brand. - Brand override layer (
brand-{name}.json): only the diffs, typically the color palette and primary typeface. A brand file with no overrides is empty and simply inherits core. - Component override layer: for the rare case where a brand needs a specific component to diverge beyond what a token override can express (say, Brand C's button always has a pill radius regardless of the shared
radius.mdtoken), an override lives at the component-token level (button.radiusfor Brand C specifically), not by forking the component's code.
Resolution order
A component always resolves component-override, then brand-override, then core, falling back one tier at a time, so a brand with no button override still gets the correct shared default.
Build and publish
- Style Dictionary (or equivalent) merges the three tiers per brand and emits platform artifacts (Web CSS variables, iOS Swift constants, Android XML resources) for each.
- Each brand publishes as its own versioned npm package (
@org/tokens-brand-a), all depending on a shared@org/tokens-corepackage so a core-level fix (say, a spacing bug affecting all five brands) is a single change that flows to every brand's next release.
Worked example
button.background resolution for two brands:
| Tier | Brand A | Brand B |
|---|---|---|
| Core | color.surface.primary = #2563EB | color.surface.primary = #2563EB |
| Brand override | color.surface.primary = #0EA5E9 (Brand A overrides to a lighter blue) | (no override, inherits core) |
| Component override | (none) | (none) |
Resolved button.background | #0EA5E9 | #2563EB |
Brand A's override cascades to button.background automatically because the component references the semantic token, not a hardcoded value, that's the point of the semantic layer: Brand A never had to touch a button-specific token to change its primary color everywhere the semantic token is used.
Trade-offs & pitfalls
Three resolution tiers means tracing a component's final value by eye gets harder as overrides accumulate, without tooling (a "resolve this token for this brand" CLI or Figma plugin) engineers will resort to reading generated CSS output to debug a color, which defeats the point of the abstraction. The most common failure mode at this scale is a brand team patching a component's code directly to fix a visual mismatch instead of adding a token override, that fix works for their build but silently forks the component from the shared library, and the next core update won't reach them. Publishing five separate packages also adds real release coordination overhead versus a single package with a brand prop, that trade-off is worth it here because five independently deployed brands are exactly the case where isolated release cadences (Brand A shipping a fix without waiting on Brand B's QA cycle) matter more than a single unified release.
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.