Skip to main content

Design systems engineer coding interview questions

11 questions on coding for design systems engineer candidates. Expect prompts such as “Flatten a nested design token object into CSS custom properties” and “Implement a compound Tabs component using React context”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 11 of 11 coding questions.

As asked

Given a nested JavaScript object of design tokens, write a function that outputs a flat array of CSS custom property declarations. For example, { color: { primary: { base: '#007bff' } } } should produce ['--color-primary-base: #007bff']. Handle arbitrary nesting depth.

Sample answer outline

A recursive function that tracks the current path prefix and recurses on objects, writing a CSS custom property string when it reaches a leaf value. Edge cases include null values, arrays (which should probably be skipped or joined), and numeric values like 0 that are falsy. A clean answer separates the traversal from the string formatting and handles the prefix concatenation with a separator that is consistent (dash-joined).

Reference implementation (typescript)

TypeScript
const tokens = {
  color: {
    primary: { base: '#007bff', hover: '#0056b3' },
    neutral: { 100: '#f5f5f5', 900: '#111111' }
  },
  spacing: { 1: '4px', 2: '8px' }
};

function flattenTokens(obj, prefix = '-') {
  // TODO: return string[]
}

console.log(flattenTokens(tokens));
// Expected output:
// ['--color-primary-base: #007bff', '--color-primary-hover: #0056b3', ...]

Expect these follow-ups

  • How would you modify this to output Swift constants instead of CSS custom properties?
  • How would you handle a token value that is itself an object, like a shadow token with x, y, blur, and color keys?
design-tokensjavascriptrecursioncss-custom-properties

As asked

Implement a Tabs compound component with Tabs, TabList, Tab, and TabPanel subcomponents that share state via React context. The active tab should be tracked in Tabs, and Tab should read its active state from context without prop drilling. Include keyboard navigation: arrow keys move between tabs.

Sample answer outline

A TabsContext holds the active tab id and a setter. TabList renders a div with role=tablist and listens for arrow key events to shift the active tab. Each Tab receives its id as a prop, reads the active state from context, and renders with role=tab, aria-selected, and aria-controls pointing to its panel. TabPanel uses aria-labelledby pointing back to its tab. A strong answer also handles roving tabindex (only the active tab is in the tab order, others use tabindex=-1) for correct keyboard navigation per the ARIA Authoring Practices Guide.

Reference implementation (typescript)

TypeScript
import React, { createContext, useContext, useState } from 'react';

interface TabsCtx {
  activeId: string;
  setActiveId: (id: string) => void;
}

const TabsContext = createContext<TabsCtx | null>(null);

export function Tabs({ defaultId, children }: { defaultId: string; children: React.ReactNode }) {
  const [activeId, setActiveId] = useState(defaultId);
  return <TabsContext.Provider value={{ activeId, setActiveId }}>{children}</TabsContext.Provider>;
}

export function Tab({ id, children }: { id: string; children: React.ReactNode }) {
  const ctx = useContext(TabsContext)!;
  const isActive = ctx.activeId === id;
  return (
    <button
      role="tab"
      aria-selected={isActive}
      tabIndex={isActive ? 0 : -1}
      onClick={() => ctx.setActiveId(id)}
    >{children}</button>
  );
}

Expect these follow-ups

  • How would you make this component controlled (externally managed active tab) as well as uncontrolled?
  • How would you add support for vertical tab lists where Up/Down arrows navigate instead of Left/Right?
reactcompound-componentscontextaccessibilitykeyboard-navigation

As asked

Write a function that takes two hex color strings and returns the WCAG 2.x contrast ratio between them as a number. Then write a second function that takes the contrast ratio and returns whether the pair passes AA for normal text, AA for large text, or AAA for normal text.

Sample answer outline

The solution requires parsing hex to RGB, linearizing sRGB values (divide by 255, then apply the piecewise gamma correction formula), computing relative luminance with the weighted sum, and applying the contrast ratio formula (L1+0.05)/(L2+0.05). AA normal text requires 4.5:1, AA large text requires 3:1, AAA normal text requires 7:1. A strong answer handles three-digit hex shorthand (#RGB), caches the linearization calculation, and returns an enum or object describing which levels pass.

Reference implementation (typescript)

TypeScript
function hexToRelativeLuminance(hex: string): number {
  // Parse hex, linearize sRGB, apply weights
  // L = 0.2126R + 0.7152G + 0.0722B
}

function contrastRatio(hex1: string, hex2: string): number {
  const l1 = hexToRelativeLuminance(hex1);
  const l2 = hexToRelativeLuminance(hex2);
  const [lighter, darker] = l1 > l2 ? [l1, l2] : [l2, l1];
  return (lighter + 0.05) / (darker + 0.05);
}

type WCAGLevel = 'AA-normal' | 'AA-large' | 'AAA-normal' | 'fail';
function wcagLevel(ratio: number): WCAGLevel[] {
  // TODO
}

Expect these follow-ups

  • How would you extend this to suggest an adjusted color that meets AA if the current pair fails?
  • How does alpha transparency in the foreground or background color affect this calculation?
accessibilitycolor-sciencewcagjavascriptalgorithms

As asked

Write a React hook called useFocusTrap that, when active, constrains keyboard focus to the elements within a given container ref. Tab should cycle forward, Shift+Tab should cycle backward, and it should move focus to the first focusable element on activation.

Sample answer outline

The hook attaches a keydown listener on the document (or the container) that intercepts Tab and Shift+Tab when active. It queries all focusable selectors (a, button, input, select, textarea, [tabindex]:not([tabindex='-1'])) from the container, finds the first and last, and wraps focus accordingly. On activation, it saves the previously focused element and moves focus to the first focusable child. On deactivation, it returns focus to the saved element. A strong answer also handles the edge case where there are no focusable children and keeps focus on the container itself.

Reference implementation (typescript)

TypeScript
import { useEffect, useRef } from 'react';

const FOCUSABLE = [
  'a[href]', 'button:not([disabled])', 'input:not([disabled])',
  'select:not([disabled])', 'textarea:not([disabled])',
  '[tabindex]:not([tabindex="-1"])'
].join(',');

export function useFocusTrap(
  containerRef: React.RefObject<HTMLElement>,
  active: boolean
) {
  const previousFocus = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (!active || !containerRef.current) return;
    // TODO: save focus, move to first focusable, trap Tab/Shift+Tab
  }, [active, containerRef]);
}

Expect these follow-ups

  • How would you handle a focusable element inside the trap that has tabindex=-1 set dynamically at runtime?
  • How would you test this hook using Testing Library without spinning up a full browser?
accessibilityreact-hooksfocus-managementkeyboard-navigationjavascript

As asked

Implement a Box component in TypeScript that accepts an as prop determining the HTML element or React component it renders as. When as='a', href should be a valid required prop. When as='button', type should be available. The component should forward refs correctly.

Sample answer outline

The implementation uses a generic ElementType type parameter, ComponentPropsWithoutRef to merge native element props, and forwardRef with a cast to handle the generic ref mismatch in TypeScript. The key challenge is that TypeScript cannot infer the generic inside forwardRef natively, so the forwardRef return type needs to be cast or the displayName added via a wrapper. A strong answer also handles the default element type (div) when as is omitted.

Reference implementation (typescript)

TypeScript
import React from 'react';

type PolymorphicProps<E extends React.ElementType, Own = {}> =
  Own & Omit<React.ComponentPropsWithoutRef<E>, keyof Own> & { as?: E };

type BoxProps<E extends React.ElementType = 'div'> = PolymorphicProps<E, {
  className?: string;
}>;

// forwardRef generic workaround:
export const Box = React.forwardRef(
  <E extends React.ElementType = 'div'>(
    { as, ...props }: BoxProps<E>,
    ref: React.Ref<unknown>
  ) => {
    const Tag = as ?? 'div';
    return <Tag ref={ref} {...props} />;
  }
) as <E extends React.ElementType = 'div'>(
  props: BoxProps<E> & { ref?: React.Ref<unknown> }
) => React.ReactElement;

Expect these follow-ups

  • Why can't you use React.forwardRef directly with a generic type parameter without casting?
  • How does the Radix UI asChild pattern solve the same problem with a different API?
typescriptpolymorphic-componentsreactgenericsapi-design

As asked

A consumer reports that their theme override is not working. They have set --color-button-bg: green on a [data-theme='custom'] attribute selector, but the button still shows the default blue. Identify the most likely causes and write a minimal reproduction that demonstrates the issue and the fix.

Sample answer outline

Most likely causes: the component reads the token from :root and the data-theme attribute is not an ancestor of the component, or the CSS custom property is defined inside a @layer with lower priority than the consumer's stylesheet. The fix is to ensure the component reads var(--color-button-bg, var(--color-action-primary-default)) from the nearest ancestor, not :root. A strong candidate writes a minimal HTML file that demonstrates the problem with a scoped data-theme on a div and shows the computed style mismatch using getPropertyValue.

Reference implementation (css)

css
/* Consumer expects this to turn the button green */
[data-theme='custom'] {
  --color-button-bg: green;
}

/* Component stylesheet */
:root {
  --color-button-bg: blue; /* This wins - why? */
}
.button {
  background-color: var(--color-button-bg);
}

/* HTML */
<body data-theme="custom">
  <button class="button">Click me</button>
</body>

/* Fix: move the default to a lower-priority location */

Expect these follow-ups

  • If you discovered the token was being read from :root because of a third-party CSS reset, how would you fix it without modifying the reset?
  • How would you write a unit test that catches this kind of token scope regression?
css-custom-propertiesthemingspecificitydebuggingcascade

As asked

Write a Storybook global decorator in TypeScript that adds a theme toolbar item with Light, Dark, and High-Contrast options. When the user selects a theme, it sets a data-theme attribute on the story's root element so CSS custom property themes apply correctly.

Sample answer outline

The decorator uses Storybook's useGlobals hook to read the selected theme from the global state set by the toolbar addon (@storybook/addon-toolbars). It wraps the story in a div with the data-theme attribute set dynamically. The .storybook/preview.ts file registers the toolbar item under globals and globalTypes. A strong answer includes the full shape of globalTypes (defaultValue, toolbar with items array and title), the decorator function signature, and handles the case where the theme is undefined by falling back to light.

Reference implementation (typescript)

TypeScript
// .storybook/preview.ts
import type { Preview, Decorator } from '@storybook/react';
import { useGlobals } from '@storybook/preview-api';
import React from 'react';

export const globalTypes = {
  theme: {
    name: 'Theme',
    defaultValue: 'light',
    toolbar: {
      icon: 'paintbrush',
      items: ['light', 'dark', 'high-contrast'],
      showName: true
    }
  }
};

const withTheme: Decorator = (Story, context) => {
  const [{ theme }] = useGlobals();
  return (
    <div data-theme={theme ?? 'light'} style={{ padding: 16 }}>
      <Story />
    </div>
  );
};

Expect these follow-ups

  • How would you persist the selected theme across story navigation so it doesn't reset when you switch to a new story?
  • How would you make this decorator also apply a class to the html element for themes that target html rather than the component root?
storybookthemingdecoratorstypescriptdocumentation

As asked

Write a function that takes two design token flat maps (old and new) and returns a structured diff describing added tokens, removed tokens, and changed values. The output should be suitable for generating a CHANGELOG entry.

Sample answer outline

The function compares keys across the two flat maps: keys only in old are removed, keys only in new are added, keys in both where values differ are changed. A strong implementation returns a typed object with added, removed, and changed arrays, where changed includes both the old and new value. The candidate should also consider that values might be objects (for complex tokens like shadows) and use deep equality rather than string comparison. The output format should group by token category prefix for readable changelogs.

Reference implementation (typescript)

TypeScript
type TokenMap = Record<string, string | number>;

interface TokenDiff {
  added: { key: string; value: string | number }[];
  removed: { key: string; value: string | number }[];
  changed: { key: string; oldValue: string | number; newValue: string | number }[];
}

export function diffTokens(oldTokens: TokenMap, newTokens: TokenMap): TokenDiff {
  // TODO: compute added, removed, changed
}

// Example:
// const old = { '--color-primary': '#007bff', '--spacing-1': '4px' };
// const next = { '--color-primary': '#0066cc', '--spacing-2': '8px' };
// diffTokens(old, next)
// => { added: [{key:'--spacing-2',...}], removed:[{key:'--spacing-1',...}], changed:[{key:'--color-primary',...}] }

Expect these follow-ups

  • How would you integrate this into a CI pipeline so that any PR touching the token file automatically posts a diff as a PR comment?
  • How would you flag a token rename (where both a removal and an addition have the same value) as a rename rather than two separate events?
design-tokenstoolingchangelogjavascriptci

As asked

Implement a Stack component that accepts a gap prop that can be a single token value OR a responsive object like { base: '4', md: '8', lg: '12' }. The component should generate the correct CSS for each breakpoint using your token system. Show how you would implement this without media query duplication.

Sample answer outline

A strong solution defines a ResponsiveProp<T> type as T | Partial<Record<Breakpoint, T>>. The component uses a utility function that normalizes single values to the object form, then maps each breakpoint to a CSS class or inline style. For a CSS-first approach, each responsive value sets a CSS custom property on the element at a given media query breakpoint, and the component reads var(--gap) which is overridden at each breakpoint. This avoids generating a new class per value. The candidate should mention that a CSS-in-JS approach like Panda CSS handles this with the token() utility and atomic style generation.

Reference implementation (typescript)

TypeScript
type Breakpoint = 'base' | 'sm' | 'md' | 'lg' | 'xl';
type ResponsiveProp<T> = T | Partial<Record<Breakpoint, T>>;

interface StackProps {
  gap?: ResponsiveProp<string>;
  children: React.ReactNode;
}

const breakpoints: Record<Exclude<Breakpoint, 'base'>, string> = {
  sm: '640px', md: '768px', lg: '1024px', xl: '1280px'
};

export function Stack({ gap = '4', children }: StackProps) {
  const gapObj = typeof gap === 'string' ? { base: gap } : gap;
  // TODO: generate inline style or class names for each breakpoint
  return <div style={{ display: 'flex', flexDirection: 'column' }}>{children}</div>;
}

Expect these follow-ups

  • How does this approach handle server-side rendering where the breakpoint is not yet known?
  • How would you extend this to support direction-aware responsive props like paddingInline that also need to flip for RTL?
responsive-designcomponent-librarytypescriptcssapi-design

As asked

Write a function that takes a single hex color and generates a 10-step scale (50 through 900) where each step is perceptually equidistant in lightness. Use the OKLCH color space for perceptual uniformity rather than raw HSL lightness interpolation.

Sample answer outline

The function converts the input hex to OKLCH (via linear sRGB to Oklab to OKLCH), extracts the lightness channel, then generates 10 target lightness values evenly spaced between near-white and near-black, keeping the chroma and hue from the input color at each step. The output is converted back to hex or oklch() strings. A strong answer explains why HSL hue rotation causes perceptual hue shift (the banana problem) and why OKLCH maintains chroma-adjusted hue. Libraries like culori or chroma.js provide the color space math.

Reference implementation (typescript)

TypeScript
import { converter, formatHex } from 'culori';

const toOklch = converter('oklch');
const toHex = formatHex;

export function generateColorScale(baseHex: string): Record<string, string> {
  const base = toOklch(baseHex);
  if (!base) throw new Error('Invalid color');

  const steps = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
  const lightnessRange = [0.97, 0.15]; // light to dark

  return Object.fromEntries(
    steps.map((step, i) => {
      const t = i / (steps.length - 1);
      const lightness = lightnessRange[0] + t * (lightnessRange[1] - lightnessRange[0]);
      // TODO: construct oklch color and convert to hex
      return [step, ''];
    })
  );
}

Expect these follow-ups

  • How would you ensure that step 400 on a neutral gray scale meets AA contrast when used as text on white?
  • How do you handle a highly saturated input color where the full 10-step range would go out of the sRGB gamut?
color-scienceoklchdesign-tokensjavascriptalgorithms

As asked

Write a Node.js script using the TypeScript compiler API or Babel AST to scan a directory of .tsx files and report all JSX style prop inline objects that contain hardcoded color values (hex strings or rgb() literals) instead of design token references.

Sample answer outline

Using Babel with @babel/parser and @babel/traverse, visit JSXAttribute nodes where the name is 'style', then traverse the ObjectExpression value looking for StringLiteral or TemplateLiteral values that match color patterns. A strong implementation builds a regex for hex and rgb(), reports the file, line, and the specific property name, and formats output as JSON or as ESLint-compatible diagnostics so it can be consumed by CI. The candidate should mention that this approach only catches inline styles, not className strings or CSS files, and that a Stylelint plugin handles the CSS side.

Reference implementation (typescript)

TypeScript
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import { readFileSync, readdirSync } from 'fs';

const COLOR_RE = /#([0-9a-fA-F]{3,8})|rgb\(/;

function scanFile(filePath: string) {
  const code = readFileSync(filePath, 'utf-8');
  const ast = parse(code, { sourceType: 'module', plugins: ['typescript', 'jsx'] });

  traverse(ast, {
    JSXAttribute(path) {
      if ((path.node.name as any).name !== 'style') return;
      // TODO: walk ObjectExpression values, match COLOR_RE, report violations
    }
  });
}

// Usage: scanDirectory('./src')

Expect these follow-ups

  • How would you extend this scanner to also catch raw pixel values in spacing properties like margin or padding?
  • How would you package this as an ESLint plugin so it runs in the editor in real time rather than as a separate script?
astbabellintingdesign-tokenstooling