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.
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.
Describe practical strategies for building responsive components inside a design system, especially for a component that needs to look right both in a narrow sidebar and in a full-width page section. Discuss the different techniques you'd reach for and when each one applies. Explain how you'd document responsive behavior so designers and engineers implement consistent rules.
Sample Answer
Direct answer
Reach for container queries when a component needs to respond to the space it is actually placed in (a card that looks different in a narrow sidebar versus a full-width section), viewport breakpoints when the whole page layout needs to shift together, and fluid scaling (clamp()) for smooth adjustments like type size or padding between those breakpoints. Document the rule as part of each component's spec, not as a separate, easily-forgotten page, so designers and engineers implement the same behavior without re-deriving it per component.
Structured elaboration
Techniques and when each applies
| Technique | Responds to | Best for | Limitation |
|---|---|---|---|
| Viewport breakpoints (media queries) | Overall browser/viewport width | Page-level layout shifts: navigation collapsing, grid column count changing | Cannot express "this component is narrow because it's in a sidebar," since it only sees the viewport, not its own container |
| Container queries | The size of the component's own containing element | A component that must adapt identically whether it's in a 300px sidebar or a 900px full-width section | Needs the component to sit inside an element with container-type set, which is an intentional layout decision the parent has to make |
Fluid scaling (clamp(), min()/max()) | Continuous interpolation between a minimum and maximum value | Typography, padding, and gaps that should scale smoothly instead of jumping at a fixed breakpoint | Not a substitute for structural layout changes (switching from a stacked to a side-by-side arrangement still needs a breakpoint or container query) |
Container queries versus global media queries for a shared component
A component in a design system is reused in contexts the component itself does not control, a dashboard widget, a sidebar card, a full-width hero. A viewport media query answers "how wide is the browser window," which tells you nothing about how wide this specific instance is. A container query answers "how wide is the element I actually have to render into," which is the question a reusable component actually needs answered. For genuinely page-level decisions (does the whole app switch to a mobile nav), a viewport media query is still the right tool, since there is no meaningful "container" above the page itself.
Browser support and fallback
Container queries now have broad support across current evergreen browsers (Chrome, Firefox, Safari, Edge), so for most product surfaces no fallback is required. A fallback is only a real concern when a specific supported environment still uses an older engine (an embedded webview pinned to an old OS version, for example). In that narrow case, degrade gracefully rather than blocking the feature: feature-detect with @supports (container-type: inline-size) and fall back to a fixed, conservative layout (the narrow-container variant) rather than a broken one, or use a ResizeObserver-based JavaScript fallback only if that specific environment must be supported and container queries genuinely are not available there.
Documenting responsive behavior
Add a "Responsive behavior" section to each component's spec, alongside its props table, that states: which technique is used (breakpoint, container query, or fluid scale), the specific trigger values, which visual properties change, and a screenshot or embed at two or three representative sizes. Keeping this next to the prop documentation, rather than in a separate cross-cutting responsive-design guide, means an engineer implementing the component sees the rule at the point of use instead of needing to remember a separate reference.
Worked example
A Card component needs to look right both in a 320px sidebar and a 900px full-width section.
container-type: inline-sizeis set on theCard's wrapper so the component can query its own rendered width, independent of the page's viewport width.- Below a 420px container width,
Cardstacks its image above its text (narrow layout); at or above 420px, it switches to image-beside-text (wide layout). This threshold is expressed as a container query, not a viewport media query, so the sameCardinstance renders correctly in a 320px sidebar and would also render the wide layout correctly if that same sidebar were later widened to 500px, without any change to the surrounding page layout. - The
Card's internal padding usesclamp(12px, 4cqi, 20px)(container-query-relative units) so padding scales smoothly with the container's width instead of jumping abruptly at the 420px threshold. - The component spec documents this as: "Stacks below 420px container width, switches to side-by-side at or above 420px; padding scales fluidly between 12px and 20px based on container width," with a screenshot at 320px, 420px, and 900px.
Trade-offs and pitfalls
- Using a viewport media query for a component-level layout decision is the most common mistake; it works by coincidence when the component happens to fill most of the viewport, and breaks silently the first time the same component is reused in a narrower context like a sidebar or a modal.
- Overusing fluid scaling for structural changes (trying to
clamp()a layout from stacked to side-by-side) produces awkward in-between states; reserve fluid scaling for continuous properties like size and spacing, and use a container query or breakpoint for discrete layout switches. - Documenting responsive rules only in a general design-system guide, separate from the component's own spec, means the rule gets missed by whoever implements or modifies that specific component later; keep the rule attached to the component it governs.
- Setting
container-typeon every wrapper "just in case" has a real performance cost (it constrains layout containment); apply it deliberately to the specific containers whose components actually need to query their own size.
Describe the difference between raw palette values (e.g., brand-500 or #0052cc) and semantic color tokens (e.g., 'button-primary-bg' or 'surface-default'). Provide three concrete mappings from raw palette entries to semantic tokens and explain when to use raw values versus semantic tokens.
Sample Answer
Direct answer
A raw palette value names what a color is (brand-500, #0052CC); a semantic token names what a color is for (button-primary-bg, surface-default). Components should reference semantic tokens, not raw values, so that changing what "primary" means, or swapping the whole palette for a theme, is a change in one mapping layer instead of a find-and-replace across every component.
Structured elaboration
The two layers
- Raw palette: a fixed scale of color values, usually organized by hue and lightness step (
neutral-0throughneutral-900,brand-100throughbrand-900). This layer defines the available colors but says nothing about where they are used. - Semantic tokens: named by role or intent, and each one points at a raw palette value. The name describes the UI concept (
surface-default,text-secondary,border-danger), not the hue.
Three concrete mappings
| Semantic token | Raw palette value | Role |
|---|---|---|
button-primary-bg | brand-500 (#0052CC) | Background of the primary call-to-action |
button-primary-text | neutral-0 (#FFFFFF) | Text on the primary button, chosen for contrast against brand-500 |
surface-default | neutral-100 (#F4F6F8) | Default page/card background |
When to use which
- Use raw palette values only inside the token-definition layer itself, when defining or adjusting the underlying scale (adding a new hue, tuning a lightness step). A component's stylesheet should never reference a raw value directly.
- Use semantic tokens everywhere a component or a design file needs a color, because the semantic layer is what makes theme swaps (dark mode, a high-contrast mode, a white-label brand variant) a matter of re-pointing the mapping rather than editing every component.
Worked example
A dark-mode theme is added. surface-default is re-mapped from neutral-100 (#F4F6F8, light gray) to neutral-900 (#1A1D21, near-black), and button-primary-text stays mapped to neutral-0 in both themes since white still contrasts against the primary blue in either mode. No component code changes: every component that referenced surface-default or button-primary-bg picks up the new theme automatically, because they never referenced #F4F6F8 or brand-500 directly. If components had used raw hex values instead, adding dark mode would require editing every component that used a background color, one at a time.
Trade-offs and pitfalls
- Exposing raw palette tokens directly to product teams (letting them use
brand-500in a one-off screen) creates values that are invisible to the semantic layer's theme-swap logic; a dark-mode rollout then misses every place a raw value was used instead of a semantic token. - Over-abstracting into too many narrow semantic tokens (a distinct token for every single component instance) recreates the maintenance burden of raw values, just with longer names; keep semantic tokens at the level of reusable roles (
surface-default,surface-elevated) rather than one-off component names (homepage-hero-bg). - A semantic token that maps to a raw value which itself later fails an accessibility check (for example a palette-wide hue adjustment) should be caught by automated contrast testing on the token mapping, not discovered visually after a theme ships.
A product team requests a one-off visual control that doesn't align with the core system. Design an exception request workflow for such requests. Include the proposal format, risk assessment criteria, acceptance criteria, tagging mechanisms in the library, time-bounded approval, and a deprecation path if the exception is temporary.
Sample Answer
Direct answer
Treat every one-off visual as a time-boxed loan against system consistency: require a short written proposal, a risk-scored review, an explicit expiry date tagged into the component library, and a default "reabsorb into the system or remove" outcome at expiry, so exceptions never quietly become permanent.
Structured elaboration
Proposal format: title, requesting team and owner, product surface, the exact tokens/components being overridden and how, the business reason, requested duration, and the reviewers needed to sign off.
Risk assessment criteria: visual scope (one screen vs. shared chrome), accessibility impact (contrast, keyboard, screen reader), performance cost (custom assets/animations), and brand-safety, since a common trigger for these requests is a marketing campaign that needs to diverge from the core look for a limited run.
Acceptance criteria: must still pass WCAG AA contrast and keyboard operability, must be scoped behind a feature or campaign flag so it can be pulled independently of a full release, and must have a named owner responsible for the exception's lifecycle.
Tagging mechanism: the component or override carries metadata visible in the docs site or Storybook: status: exception, owner, expires: <date>, reason. A visible badge distinguishes exception components from sanctioned ones so nobody mistakes a one-off for a supported pattern.
Time-bounded approval: review SLA scales with risk (48-72 hours for low-risk, longer for anything touching shared chrome). Approval grants a default time-to-live, for example 90 days for a typical campaign exception; renewal requires re-justifying the request, not an automatic rollover.
Deprecation path: an automated reminder fires before expiry. At expiry, the design-system team checks reuse: if the pattern has been independently requested by other teams, it's a candidate for promotion into the core system through the normal review workflow; if it was a genuine one-off, it gets scheduled for removal, ideally with a codemod or scripted migration back to the sanctioned pattern.
flowchart TD
A[Proposal submitted] --> B[Risk review]
B -->|Rejected| C[Use existing pattern]
B -->|Approved, time-boxed| D[Tagged + TTL clock starts]
D --> E[In use]
E --> F{Expiry reached}
F -->|Reuse count over threshold| G[Promote to core]
F -->|Low reuse| H[Deprecate and remove]
F -->|Owner requests renewal| I[Re-justify and reset TTL]
I --> D
Worked example
Marketing requests a one-off animated gradient CTA button for a holiday campaign landing page, needed for a six-week run. Risk review: visual scope is one page (low), contrast is checked and passes AA, no shared-chrome exposure. Approved with a 45-day TTL (the six-week campaign plus a short buffer). Tagged exception:campaign-holiday-2026, owner:marketing-design, expires:2026-09-01. At expiry, the reuse count is checked: only the one campaign page used the pattern, so the decision is deprecate and remove, with the campaign page reverting to the standard Button once the campaign itself has ended.
Trade-offs & pitfalls
No default expiry means every exception is permanent by default, which is how design systems accumulate silent debt. An overly heavy proposal process for genuinely low-risk one-offs kills the escape valve entirely and pushes teams toward shadow CSS that bypasses the system with no review at all. A recurring pitfall is not tracking reuse count, which means the design-system team loses the signal that would tell them a pattern is actually worth promoting. Another is an exception with no living owner: if the original requester leaves the team, the TTL renewal has nobody to act on it, so the process needs an escalation path that flags unowned exceptions to the design-system team directly rather than letting them silently auto-renew.
Explain why design system adoption matters for product teams, not just for the design system team. Give concrete examples of what changes for a product team once they actually adopt it versus staying off it, and list the common pitfalls you see when adoption is shallow or uneven across an organization.
Sample Answer
Direct answer
Design system adoption matters to a product team, not just the design system team, because it changes what the team's own time gets spent on: less time re-deciding solved problems (spacing, states, accessibility wiring), more time on the parts of the product that are actually differentiated. A team that stays off the system keeps paying that re-deciding cost on every feature, indefinitely, and that cost compounds as the product grows.
Structured elaboration
What concretely changes once a team adopts
| Area | Before adoption (or shallow adoption) | After real adoption |
|---|---|---|
| Design velocity | Every new screen starts from a blank canvas: spacing, type scale, and button styles get re-decided each time | New screens compose from existing, pre-decided components; design time goes into layout and flow, not re-litigating a button's padding |
| Engineering velocity | Front-end engineers hand-roll markup and CSS per feature, including accessibility wiring, every time | Engineers import tested components with accessibility already handled; a bug fixed once in the shared component is fixed everywhere it's used |
| Quality and consistency | Visual and interaction drift accumulates screen by screen, invisible until a user notices two different-looking buttons doing the same thing | Interaction patterns (hover, focus, loading, error states) behave the same way across the product, so a user's learned behavior transfers between screens |
| Maintenance cost | A rebrand or accessibility fix requires finding and updating every hand-built instance individually | The same change happens once, in the shared component, and every consuming screen inherits it automatically |
Common pitfalls of shallow or uneven adoption
- Fragmentation: teams that only partially adopt create local, near-duplicate variants of existing components, which produces visible drift instead of the consistency the system was supposed to deliver.
- Incomplete documentation on the team's side: if the team doesn't understand a component's intended states or composition points, they'll misuse it or route around it entirely.
- Ownership gaps: components nobody clearly owns go stale, and teams learn (correctly) that they can't rely on the system staying current, which further discourages adoption.
- Tooling mismatch: if design tokens aren't actually synced into the codebase the team ships from, "using the design system" in Figma and "using the design system" in production silently diverge.
- No incentive to migrate: without some structural nudge (a linting rule, a review checklist, leadership visibility into adoption), teams under deadline pressure will always default to the fastest hand-rolled path over the system's slightly higher up-front learning cost.
Worked example
A single shared Button component that ten product screens depend on gets an accessibility fix: its focus ring wasn't visible enough against the current color tokens, so it's updated once inside the design system's codebase. Every one of those ten screens picks up the fix the next time they update their dependency on the component library, with zero additional design or engineering work on any individual screen. Contrast that with a team that only shallowly adopted the system and hand-built three "Button-like" elements outside it: that same accessibility fix now has to be found, understood, and manually re-applied three separate times, by someone who has to first realize those three elements exist and aren't the real component.
Trade-offs & pitfalls
The main trap in explaining "why adoption matters" is describing benefits that only accrue to the design-system team (fewer support requests, cleaner Figma files) rather than benefits the product team itself feels directly; if a product team can't see what changes for them, the pitch won't land regardless of how real the benefit is elsewhere. The second trap is treating adoption as binary: a team that imports the button component but still hand-rolls its own spacing and typography has adopted the easiest 10% and kept the other 90% of the drift risk, which is exactly the shallow-adoption failure mode this question is pointing at.
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.