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.
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.
As a UI designer, what baseline accessibility checks should you enforce for every design-system component before it gets published? List at least five checks and briefly explain their importance.
Sample Answer
Direct answer
At minimum, every component should be checked for color contrast, full keyboard operability, correct semantic role and accessible name, visible focus, accessible labeling of state and errors, and adequate touch-target size, each maps to a concrete WCAG success criterion and a test you can run in minutes.
Structured elaboration
| Check | Why it matters |
|---|---|
| Color contrast (4.5:1 body text, 3:1 large text and UI components, WCAG AA) | Below this ratio, text and controls become illegible for users with low vision, and it's the single most common accessibility failure across component libraries |
| Full keyboard operability (Tab reaches every interactive element; Enter/Space activates; Escape closes overlays; arrow keys move focus within composite widgets like a listbox or tabs) | Keyboard-only and screen-reader users cannot use a mouse; if a control can't be reached or operated by keyboard, it's unusable for them, not just harder |
Correct semantic role and accessible name (native HTML element, or the matching ARIA role plus a name via <label> or aria-label) | Assistive technology announces the role and name, not the visual appearance; without them a screen reader user has no idea what a control is or does |
| Visible focus indicator (never remove the default outline without a compliant replacement, per WCAG 2.2's focus-visibility criteria) | Sighted keyboard users rely entirely on the focus indicator to know where they are on the page |
Accessible error and state announcement (aria-invalid plus aria-describedby pointing at the error text, or aria-live for async status) | A validation error that's only shown as a red border is invisible to a screen reader user unless it's also announced or linked to the field |
| Touch target size (roughly 24-44 CSS px minimum, with spacing between adjacent targets) | Small, cramped targets are hard to hit accurately for users with motor impairments and for anyone on a touchscreen |
Worked example
Applying the checklist to a custom Dropdown/Select: contrast passes on both the trigger and the option list against the background. Keyboard: Tab focuses the trigger, Enter/Space/Down-arrow opens it, Arrow keys move the active option using aria-activedescendant or roving tabindex, Escape closes it and returns focus to the trigger rather than dropping focus onto the page body. Semantics: the trigger is a real <button>, the list uses role="listbox" with role="option" children and aria-selected on the active one. Focus management: while options are being announced, focus visibly stays on the trigger element rather than jumping into the list, which is what most screen readers expect for this pattern. Each of those becomes a pass/fail row a reviewer can check off in a few minutes.
Trade-offs & pitfalls
Relying only on automated scanning (axe or similar) misses real problems: an automated tool can confirm a label exists, but it cannot tell you whether the tab order is logical or whether an aria-live announcement is actually useful when it fires, those need a short manual keyboard and screen-reader pass. A common pitfall is adding tabindex="0" to a non-interactive element to "make it accessible": that adds noise to the tab order without adding any real keyboard interaction, and is often a sign the underlying element should have been a native interactive element in the first place.
Design a responsive grid system that will be part of the design system and used across multiple products with very different content densities. Decide what the grid actually needs to specify and how components should consume it. Explain your approach to responsiveness (mobile-first) and accessibility considerations for content order.
Sample Answer
Direct answer
The grid needs to specify four things as tokens, not hard-coded numbers: column count, gutter width per breakpoint, container margin/max-width per breakpoint, and a span/offset primitive that components use to say "I take 4 of 12 columns" instead of a pixel width. Components consume the grid through a thin layout primitive (a Grid/Col pair or equivalent CSS classes), never by reading breakpoint values directly, so the same Card component can be dense in a dashboard and roomy in a marketing page without code changes. Responsiveness is mobile-first (styles default to the smallest viewport, min-width media queries add columns going up), and content order is an accessibility requirement, not a visual nicety: the DOM order must equal the reading order regardless of how the grid visually rearranges things.
Structured elaboration
What the grid specifies
| Token | Purpose | Example values |
|---|---|---|
grid.columns | Total divisions per row | 12 (divisible by 2, 3, 4, and 6, which covers most real layouts) |
grid.gutter.{bp} | Space between columns, per breakpoint | mobile 12px, tablet 16px, desktop 24px |
grid.margin.{bp} | Space between the container edge and the first/last column | mobile 16px, tablet 24px, desktop 32px |
grid.container.maxWidth.{bp} | Caps line length on very wide screens | tablet 720px, desktop 1024px, wide 1280px |
How components consume the grid
Components never read breakpoint values directly. They declare a span (and optionally an offset) per breakpoint through a shared primitive, and the grid layer resolves that into actual CSS. Two implementations both work: CSS Grid (grid-template-columns: repeat(12, 1fr), components use grid-column: span N) or Flexbox (flex-basis computed from span/columns). CSS Grid is the more direct mapping to "12 columns" as a literal concept and handles gaps natively via gap, so it's the better default; Flexbox remains useful for one-off rows that don't need the full grid semantics.
Responsiveness (mobile-first)
Default styles target the smallest viewport (single stacked column, no explicit span needed). Each min-width media query adds capability going up, it never removes it:
.card { grid-column: span 12; } /* mobile: full width, default */
@media (min-width: 768px) {
.card { grid-column: span 6; } /* tablet: half width */
}
@media (min-width: 1024px) {
.card { grid-column: span 4; } /* desktop: third width */
}
This means a component that never gets a media-query override still renders correctly (full-width stack) instead of breaking, which is the core mobile-first guarantee.
Accessibility and content order
The grid can visually rearrange content (grid-column/grid-row placement, or the CSS order property), but the DOM order is what screen readers and keyboard Tab order follow, not the visual order. If a design calls for a visual reorder (e.g. an image appears above the title on mobile but beside it on desktop), that reorder must be done with a placement property that doesn't change reading order, or it must be verified with an actual keyboard-only and screen-reader pass that the experience still makes sense. order in particular is a common accessibility trap: it changes what sighted keyboard users see next but not what Tab visits next, so a form that's visually reordered can send focus somewhere unexpected.
Worked example
A minimal responsive Card consuming the grid tokens above, mobile-first, with span changing at two breakpoints and the DOM order left untouched:
:root {
--grid-columns: 12;
--grid-gutter-mobile: 12px;
--grid-gutter-tablet: 16px;
--grid-gutter-desktop: 24px;
--grid-margin-mobile: 16px;
}
.grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
gap: var(--grid-gutter-mobile);
padding-inline: var(--grid-margin-mobile);
}
.card { grid-column: span 12; }
@media (min-width: 768px) {
.grid { gap: var(--grid-gutter-tablet); }
.card { grid-column: span 6; }
}
@media (min-width: 1024px) {
.grid { gap: var(--grid-gutter-desktop); }
.card { grid-column: span 4; }
}
Three cards in this markup stack to full width on mobile, pair up two-per-row on tablet (span 6 of 12), and go three-per-row on desktop (span 4 of 12), with the HTML order (and therefore reading and tab order) identical at every breakpoint.
Trade-offs & pitfalls
- 12 columns is the default because of its divisibility, but a product with genuinely different density needs (a dense admin table vs. a spacious marketing page) sometimes benefits from a second grid definition (e.g. 12 for marketing, an 8-column denser grid for the console) rather than forcing one grid to serve both; that's a real cost (two things to maintain) so only take it if the density mismatch is severe.
- Fixed max-width containers protect line length for reading but can feel wasteful on ultra-wide monitors; centering the container and letting the background bleed full-width is the usual compromise.
- Container queries (sizing a component off its own container's width instead of the viewport) are a real advancement for components reused at different densities (a Card in a 3-column grid vs. a Card in a sidebar), but they're an escalation beyond a first grid system, not a starting requirement; most interviews are testing whether you get the tokens, mobile-first cascade, and DOM-order accessibility rule right before they expect container-query fluency.
- The most common accessibility miss is exactly the
ordertrap above: a reviewer who only checks visually will approve a layout that's broken for keyboard and screen-reader users, because it looks right and nothing errors.
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.
Design a typography token set for a product. Decide what the token categories themselves need to be and define at least three semantic text roles you'd build on top of them for common UI needs. Describe how you'd represent units and why. Provide a short JSON-like example of token keys and intended values (no need to write full JSON).
Sample Answer
Direct answer
A typography token set needs two layers: a small set of primitive tokens (family, a size scale, weight, line-height, letter-spacing) and a semantic layer on top that names roles like heading, body, and caption in terms of those primitives, so product teams pick a role instead of guessing a raw size. Sizes and line-heights are expressed in rem (root-relative), never fixed px, because rem respects the user's browser zoom and OS-level text-size preference, which is an accessibility requirement, not a nicety.
Structured elaboration
Primitive token categories
font.family: the type stack itself (brand font plus system fallbacks).font.size: a base value plus a modular scale ratio, generating a stepped set of sizes rather than ad hoc numbers.font.weight: a small enum (regular, medium, semibold), not the full 100 to 900 range, so pairing decisions are constrained.font.lineHeight: unitless multipliers (e.g.1.2,1.5), not fixed px. Unitless line-height scales proportionally with whateverfont-sizeis applied to that same element, so it survives being reused at a different size without recomputation; a fixed-px line-height breaks the moment it's reused at a larger size.font.letterSpacing: small em-based adjustments, mainly for large display text and all-caps labels.
Semantic roles (at least three, built on the primitives above)
role.display: hero/marketing headlines, largest step, tight line-height, heavier weight.role.heading: section and card titles, mid-size step, tight-ish line-height.role.body: default paragraph and UI text, base step, relaxed line-height for reading comfort.role.caption: metadata, timestamps, helper text, smallest step, still relaxed line-height so small text stays legible.
Each semantic role is a bundle (size, line-height, weight), not just a size, because a heading and a caption differ in more than font-size alone and a component should consume one role token, not three separate primitive lookups.
Why rem and unitless line-height
rem is relative to the root <html> font-size, so when a user increases their browser's default text size or applies OS-level "larger text" accessibility settings, everything sized in rem scales with them; px ignores that preference entirely. px is kept only as a documentation-time reference alongside each rem value, computed off a 16px root, never shipped as the implementation unit.
Worked example
The scale is generated, not hand-picked, so every value traces back to two numbers: a 16px base and a 1.25 ratio.
sizen=base×scalen,base=16px, scale=1.25| step | formula | px | rem |
|---|---|---|---|
| step-1 (caption) | 16×1.25−1 | 12.8px | 0.8rem |
| step0 (body) | 16×1.250 | 16px | 1rem |
| step1 (subheading) | 16×1.251 | 20px | 1.25rem |
| step2 (heading) | 16×1.252 | 25px | 1.5625rem |
| step3 (display) | 16×1.253 | 31.25px | 1.953125rem |
Token keys with intended values (Style Dictionary-style reference syntax, primitives referenced by semantic roles rather than restated):
{
"font.family.base": "Inter, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
"font.size.step-1": "0.8rem",
"font.size.step0": "1rem",
"font.size.step1": "1.25rem",
"font.size.step2": "1.5625rem",
"font.size.step3": "1.953125rem",
"font.lineHeight.tight": 1.2,
"font.lineHeight.normal": 1.5,
"font.weight.regular": 400,
"font.weight.semibold": 600,
"role.display": { "fontSize": "{font.size.step3}", "lineHeight": "{font.lineHeight.tight}", "fontWeight": "{font.weight.semibold}" },
"role.heading": { "fontSize": "{font.size.step2}", "lineHeight": "{font.lineHeight.tight}", "fontWeight": "{font.weight.semibold}" },
"role.body": { "fontSize": "{font.size.step0}", "lineHeight": "{font.lineHeight.normal}", "fontWeight": "{font.weight.regular}" },
"role.caption": { "fontSize": "{font.size.step-1}", "lineHeight": "{font.lineHeight.normal}", "fontWeight": "{font.weight.regular}" }
}
Trade-offs & pitfalls
- Too many scale steps invites decision paralysis (six size options that all look almost the same); most systems ship five to seven steps total, not a step per pixel difference designers spot in Figma.
- A single global ratio is simple to reason about but can make small text feel cramped and large text feel oversized at the extremes; some systems use a gentler ratio (1.125 to 1.2) below the base and a steeper one above it. That's a deliberate escalation, not something to reach for by default.
- Naming semantic roles after their visual size ("large", "small") instead of their purpose ("heading", "caption") is a common early mistake: it works until a redesign needs
headingto get visually smaller thanbody-large, and the names now lie about the hierarchy they encode. - Skipping the semantic layer and having components consume primitive size tokens directly is the fastest way to lose consistency: two teams pick different "close enough" primitives for what's conceptually the same role, and nothing in the system prevents it.
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.