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.
What criteria would you use to decide whether to add a new component to the design system or keep it as a product-level pattern? Provide at least five evaluation points (e.g., reuse frequency, complexity, maintenance cost).
Sample Answer
A component earns a spot in the shared design system only when it's genuinely reusable across products and the team is willing to commit to maintaining it long-term; otherwise it stays a product-level pattern, owned and evolved by the team that needs it. The decision isn't about how polished the component looks, it's a cost/benefit call between centralizing (consistency, shared maintenance) and staying local (speed, no coordination tax).
Evaluation criteria
| Criterion | What to look at | Signal to promote |
|---|---|---|
| Reuse breadth | How many products/teams need it, not just how many screens | Requested or reimplemented by 3+ teams independently |
| Consistency risk | Would divergent local implementations cause visible brand or UX inconsistency | Yes, especially for anything customer-facing and frequent |
| API complexity/stability | How many props/states/variants it needs, and how likely the shape is to keep changing | Moderate, well-understood API; still-evolving APIs are a reason to wait |
| Maintenance ownership | Who commits to accessibility fixes, bug fixes, and cross-platform parity going forward | A team or system owner is willing and resourced to own it |
| Accessibility and testability | Can it be built and tested to the system's a11y bar | Yes, or the investment to get it there is justified by reuse |
| Token/theme alignment | Does it compose cleanly from existing tokens, or does it need one-off values | Composes from existing tokens without new one-off overrides |
Worked example
A "confirmation stepper" built for one onboarding flow, used nowhere else, with a still-changing API as the team iterates on the flow, scores low on reuse breadth and API stability: keep it as a product-level pattern local to that team.
Contrast that with a "date range picker": four different teams have asked for it, and three have already built their own slightly different version (different keyboard behavior, different date-format handling). That's the profile that justifies promotion: real duplicated effort, real inconsistency risk, and a stable-enough API (pick a date range) to commit to maintaining.
Trade-offs and pitfalls
Promoting too early is the more common interview trap to name: it commits the system team's limited bandwidth to something that may never get a second consumer, and if the API changes shape once a second team's real requirements show up, that's a breaking migration paid for nothing. Promoting too late has a real cost too: every duplicate local implementation is a place accessibility bugs and visual drift can hide, and consolidating three divergent versions after the fact is more expensive than building one shared version up front would have been. The criteria above exist to make that timing call explicit and defensible rather than a gut feeling.
After ideation you want to convert recurring patterns from sketches into design system components. Describe the steps you take from low-fidelity exploration to the first component draft. Explain how you decide what becomes a token, component, or utility.
Sample Answer
Direct answer
Move from a recurring sketch to a system pattern in stages: pull out the repeated visual atoms as tokens first, then judge whether the repeated pattern is really a structure with its own behavior (a component) or just a one-off layout shortcut (a utility class), and only promote something to a governed, versioned component once it has shown up more than once with real state or interaction requirements, not just visual similarity.
Structured elaboration
- Clarify goals and constraints: confirm product intent, accessibility targets, and platform (web or mobile) before refining anything.
- Low-fidelity exploration: sketch 6 to 8 variants of the recurring pattern, focusing on hierarchy, spacing, and states, and validate quickly with stakeholders or a lightweight usability check.
- Identify atomic building blocks: break the sketches into atoms, typography, color, spacing, icons, elevation, interaction behaviors, and map which pieces actually repeat across the sketches versus which just look similar by coincidence.
- Decide token vs. component vs. utility:
| Signal | Decision |
|---|---|
| A single value that must stay consistent and theme-swappable (a color role, a spacing step) | Token |
| A composite unit with internal layout, behavior, or interaction states | Component |
| A one-off layout shortcut with too much variability to be worth naming | Utility |
The rule of thumb: if a change must propagate globally or be theme-swappable, it's a token. If it encapsulates structure plus interaction, it's a component. If it's a one-off shortcut, it's a utility, at least until it repeats.
- Build the first component draft: a high-fidelity mock plus an annotated spec showing anatomy, props (size, emphasis), interaction states, accessibility notes, and which tokens each part maps to. Run a short feasibility review with engineering before calling it done.
- Iterate and measure: ship as an early or "alpha" pattern, track actual reuse, and adjust tokens and props based on real usage and edge cases rather than guessing them up front.
Worked example
A "label, value, optional icon" row pattern (an info row) turns up in five different sketches: a profile page, a settings page, an order summary, and two others. It needs a consistent spacing token (spacing.row.gap, 8px) across all five uses, and at least one use is interactively expandable with its own focus ring. That combination, real repetition plus real interaction, is what promotes it to a component: MetaRow, with props { label, value, icon?, onPress? }. Contrast that with a divider line that shows up once, before a page footer: it has no variability and no behavior, so it stays a spacing utility class rather than becoming a component that nobody else will ever use.
Trade-offs and pitfalls
Premature componentization: turning a pattern seen once into a governed, versioned component adds real maintenance and documentation overhead for zero reuse benefit. Wait for a second or third genuine occurrence before promoting it.
Utility-class sprawl: if one-off spacing or layout tweaks never get revisited, the same visual gap ends up implemented as 6px in one place and 8px in another, spacing drift that a token would have prevented. Periodically audit utility usage for patterns that quietly repeated enough to deserve a token.
Naming a component after its first usage context, ProfileRow because it was first seen on the profile page, discourages reuse elsewhere and forces an awkward rename later. Name by structure or role (MetaRow) from the first draft, not by where it happened to first appear.
Propose a testing strategy for a component library. Decide what types of tests you actually need to give confidence that a change is safe to ship, when each type should run in your pipeline, and which tools you would use.
Sample Answer
Direct answer
A component library needs four kinds of confidence, each answering a different question: does the logic work (unit tests), do the pieces work together (integration tests), does it still look right (visual regression), and is it accessible (automated a11y checks plus periodic manual review). Run the fast, deterministic ones on every pull request and push the slower or more manual checks to a scheduled cadence, so contributors get quick feedback without every PR waiting on a full audit.
Structured elaboration
Test types, when they run, who owns them
| Test type | Confidence it gives | When it runs | Primary owner | Tooling |
|---|---|---|---|---|
| Unit | Component logic and props behave correctly in isolation | On every PR | Engineers | Jest or Vitest, React Testing Library |
| Integration | Components compose correctly (forms, theming context, layout) | On every PR | Engineers | React Testing Library, real rendering |
| Automated accessibility | Contrast, missing labels, invalid ARIA, keyboard traps | On every PR (fast subset), full audit nightly | Engineers, reviewed by designers | axe-core or jest-axe |
| Visual regression | Pixel and layout drift versus an approved baseline | On every PR for changed stories, full sweep nightly | Shared: engineers approve technical diffs, designers approve intentional visual changes | Storybook plus Chromatic or Percy |
| Manual accessibility spot checks | What automation cannot catch: screen-reader flow, focus order, real keyboard use | Before a new component or major visual change ships, not every PR | Designers and engineers together | Manual testing with a screen reader (VoiceOver, NVDA) |
Ownership matters because it decides who is blocked by a failing check: an engineer should not be the sole approver of a visual regression diff on a component's intentional redesign, and a designer should not be expected to debug a failing unit test.
Deciding what is actually necessary
Start from the question "what would let a change ship with confidence," not "what testing tools exist." A component library specifically needs to guard against three failure modes: broken behavior, broken visuals, and broken accessibility. Each failure mode maps to one of the layers above; if a proposed test does not clearly guard against one of the three, it is probably not worth the maintenance cost of writing and keeping it green.
Pipeline placement
- On every PR (fast, blocking): unit, integration, the fast automated a11y subset, and visual regression only for the stories touched by the diff.
- Nightly (slower, non-blocking for individual PRs): full visual-regression sweep across every story, theme, and viewport; a fuller accessibility audit tool pass.
- Before shipping a new component or a significant visual change (manual, gated): a short manual accessibility pass with an actual screen reader and keyboard-only navigation, since automated tools reliably catch missing labels and low contrast but do not reliably catch a confusing focus order or an unannounced state change.
Worked example
A Tabs component is being added to the library.
- Unit tests (engineer-owned) verify that arrow-key navigation moves focus between tabs and that the correct tab panel is shown for the active tab. These run on every PR in under a second.
- Integration tests verify
Tabscomposes correctly when nested inside the library'sCardcomponent, since layout-context bugs only show up in composition, not inTabsalone. jest-axeruns against the renderedTabsmarkup on every PR and would catch, for example, a missingrole="tablist"or an unlabelled tab.- A Storybook story with all tab states (active, disabled, overflow with many tabs) is snapshotted; the PR run only checks the two states the diff touched, and the full state set runs on the nightly sweep.
- Before merge, a designer and engineer pair for five minutes to tab through the component with the keyboard only and confirm focus does not visibly jump or disappear, since this is the kind of defect automated a11y tools do not reliably flag.
Trade-offs and pitfalls
- Requiring the full test suite, including a manual accessibility pass, on every single PR (including a one-line copy fix) slows contribution enough that people route around it; scale the required checks to the size and risk of the change.
- Automated accessibility tools catch a meaningful but incomplete slice of real accessibility problems (missing labels, contrast, invalid ARIA); treating a green axe-core run as "accessible" without ever doing a manual pass on new components is a common false sense of security.
- Assigning visual regression approval solely to engineers means intentional design changes get rubber-stamped by someone without the design context to judge them, and solely to designers means technical false positives (font-rendering noise) block PRs unnecessarily; shared ownership with a clear split (technical diff versus intentional visual change) avoids both failure modes.
- Skipping integration tests because "the unit tests all pass" misses the most common real-world bug class in a component library: two individually-correct components that misbehave only when composed together.
You're building a navigation drawer that behaves as an overlay on mobile and as a persistent sidebar on desktop. Walk through every accessibility consideration that changes between those two layouts, including where keyboard focus should go when the drawer opens and closes, how you'd prevent focus from escaping the drawer while it's open on mobile, and how you'd reconcile touch gestures with keyboard-only users.
Sample Answer
Direct answer
The mobile overlay drawer is modal, so it needs a full focus trap: focus moves into the drawer when it opens, Tab/Shift+Tab cycle only within it while it's open, and focus returns to the control that opened it when it closes. The desktop persistent sidebar is not modal, it's a permanent part of the page, so it must never trap focus; a keyboard user should be able to Tab from the sidebar into the main content and back as part of the normal page flow, exactly like any other section of the page. Touch gestures (swipe to open or close) are an addition on top of button and keyboard controls, never a replacement for them, since a keyboard-only user has no swipe to fall back to.
Structured elaboration
What changes between the two layouts
| Aspect | Mobile overlay | Desktop persistent sidebar |
|---|---|---|
| Modality | Modal (blocks interaction with the rest of the page while open) | Non-modal (part of the normal page, coexists with main content) |
| Focus on open | Moves into the drawer, typically to the first interactive element or a heading | No change: sidebar is already in the tab order, nothing "opens" |
| Focus trap | Yes: Tab/Shift+Tab cycle only within the drawer while open | No: Tab moves naturally between sidebar and main content |
| Focus on close | Returns to the exact control that triggered the open (the hamburger button) | N/A, sidebar doesn't close |
| ARIA role | role="dialog" (or alertdialog only if it demands immediate action, which a nav drawer doesn't) with aria-modal="true" | <nav> landmark, no dialog role, since it isn't a dialog |
Escape key | Closes the drawer, focus returns to the trigger | No defined behavior; there's nothing modal to dismiss |
| Background interaction | Inert: content behind the overlay is not reachable by keyboard or screen reader while open (inert attribute or aria-hidden on siblings) | Fully interactive at all times |
| Touch gestures | Swipe to open/close is an enhancement | Not applicable, sidebar isn't gesture-driven |
Focus trap mechanics on mobile
On open: move focus to the first meaningful focusable element inside the drawer (often the first nav link, or a close button if one is visually present), and mark everything outside the drawer inert (or aria-hidden="true" plus tabindex="-1" on focusable siblings, inert is the more complete modern approach since it also blocks pointer and find-in-page). While open: intercept Tab at the last focusable element to wrap to the first, and Shift+Tab at the first to wrap to the last, so focus can never escape into the hidden background. On close (via close button, Escape, backdrop click, or swipe): restore focus explicitly to the element that opened the drawer, don't just let it fall to <body>, or a keyboard user loses their place entirely.
Why the desktop sidebar must not do any of this
A persistent sidebar is structurally just another landmark on the page (<nav>), the same category as a header or footer. Trapping focus inside it, or moving focus into it automatically, would break the normal, predictable Tab order a keyboard user relies on to move through the whole page. The single most common accessibility bug in "responsive" nav components is reusing the mobile drawer's focus-trap logic unconditionally on desktop, silently turning a permanent sidebar into something that behaves like it's stuck open and modal.
Reconciling touch gestures with keyboard-only users
Swipe-to-open and swipe-to-close are conveniences layered on top of, never instead of, an explicit trigger button and Escape support. A gesture-only implementation (drawer only opens via swipe, no visible button) has no keyboard equivalent at all, which fails outright for keyboard-only users and for many switch-access and voice-control users whose input doesn't map to a swipe gesture. The trigger button, Escape to close, and the swipe gesture should all lead to the exact same code path (the same open/close function with the same focus management), so there's no risk of the gesture path skipping the accessibility handling the button path does correctly.
Worked example
A minimal focus-trap helper, the shape that gets wired to the mobile overlay's open/close, not the desktop sidebar:
function openDrawer(drawer, trigger) {
const focusable = drawer.querySelectorAll(
'a[href], button:not([disabled]), input, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
drawer.hidden = false;
document.getElementById('main-content').inert = true;
first?.focus();
function trapTab(event) {
if (event.key !== 'Tab') return;
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last?.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first?.focus();
}
}
function onEscape(event) {
if (event.key === 'Escape') closeDrawer(drawer, trigger);
}
drawer.addEventListener('keydown', trapTab);
drawer.addEventListener('keydown', onEscape);
}
function closeDrawer(drawer, trigger) {
drawer.hidden = true;
document.getElementById('main-content').inert = false;
trigger.focus(); // restore focus to the control that opened it
}
openDrawer and closeDrawer are the same functions called by the trigger button's click handler, the Escape listener above, and a swipe gesture handler, so all three entry points guarantee identical focus behavior instead of the gesture path silently skipping the trap or the restoration step.
Trade-offs & pitfalls
- Reusing one component implementation for both layouts without conditioning the focus-trap logic on which mode is active is the most common bug class here: it's easy to ship a drawer that traps focus correctly on mobile and then, unnoticed, does the exact same trapping on desktop where the sidebar is persistent, breaking normal keyboard navigation for every desktop user.
aria-hiddenon background siblings is the older pattern and still works, but doesn't block pointer interaction or in-page search the wayinertdoes;inertis the more complete solution where browser support allows it, witharia-hiddenplus disabledtabindexas the fallback.- Forgetting to restore focus to the specific trigger element (letting it fall back to
<body>on close) is a small-looking bug with an outsized cost for screen reader and keyboard users, who lose their position on the page and have to re-navigate from the top. - Gesture-only entry points (no visible open button, relying entirely on an edge swipe) are a common mobile-web pattern borrowed from native apps that fails accessibility outright on the web, always ship the explicit button alongside the gesture, never instead of it.
You inherit a component tree with deep nesting and lots of prop drilling for theme, locale and callbacks. Propose a refactor plan to decouple components, reduce prop drilling and keep components testable. Provide specific patterns and small API examples.
Sample Answer
Direct answer
Split the refactor by concern and by how often each value changes: use context exposed through small custom hooks for slow-changing, cross-cutting values like theme and locale, keep fast-changing callbacks close to where they're triggered through composition instead of drilling them, and keep presentational components pure so they accept explicit props or an injectable dependency object rather than reaching into context directly, which is what keeps them testable.
Structured elaboration
Refactor plan
- Audit the tree and classify what's being drilled: theme and locale are slow-changing and genuinely cross-cutting, callbacks (
onSave) are fast-changing and usually only needed by one or two leaf components. - Extract
ThemeContextandLocaleContext, each with a thin provider and a small hook (useTheme,useLocale) that exposes only what's needed, not the raw context value. - For callbacks, stop drilling through intermediate layout components; restructure so the component that owns the handler renders the leaf component directly via composition (children/JSX), so
LayoutandSidebarnever seeonSaveat all. - Keep leaf presentational components pure: they accept
theme/onSaveas explicit props (defaulting to the hook internally only at the point of use), so a unit test can render them with a plain prop object and skip wrapping them in providers. - Memoize selector output in the hooks (
useMemo) so a component reading only a slice of context doesn't re-render on unrelated context changes.
flowchart TD
A[App] --> TP[ThemeProvider + LocaleProvider]
TP --> L[Layout]
L --> SB[Sidebar]
SB --> P["Panel receives children"]
P --> BTN["Button (children slot, reads theme/locale via hooks, onSave via direct prop)"]
Worked example
// ThemeProvider.tsx
const ThemeContext = createContext(null);
export function ThemeProvider({ value, children }) {
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme(selector = (t) => t) {
const ctx = useContext(ThemeContext);
return useMemo(() => selector(ctx), [ctx, selector]);
}
// Panel.tsx: composition replaces callback drilling
function Panel({ children }) {
return <div className="panel">{children}</div>;
}
// Button.tsx: pure, testable, takes explicit props
function Button({ onSave, theme, label }) {
return (
<button style={{ color: theme.color }} onClick={onSave}>
{label}
</button>
);
}
// usage in App.tsx: onSave is composed in directly, no drilling through Layout/Sidebar
<ThemeProvider value={theme}>
<Layout>
<Sidebar>
<Panel>
<Button onSave={handleSave} theme={theme} label="Save" />
</Panel>
</Sidebar>
</Layout>
</ThemeProvider>
// test: no provider needed, Button is pure
render(<Button onSave={jest.fn()} theme={{ color: "red" }} label="Save" />);
Trade-offs & pitfalls
Putting a fast-changing value like a callback into context defeats the point of the refactor: every consumer of that context re-renders on every change, which is exactly the re-render fan-out context is prone to when it's used for the wrong kind of value. Splitting into too many tiny contexts adds "provider nesting" overhead and makes the top of the tree hard to read; a single AppProviders component that composes all the providers in one place keeps that manageable. Keeping presentational components pure (accepting explicit props instead of calling hooks internally) is what preserves testability, but it does mean an extra thin "container" layer has to wire the hook output into props, which is the deliberate cost of decoupling rendering from data access.
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.