Back to React
React
easy
mid

How would you build a reusable UI component in React?

Open-ended 'build a UI component' prompt. Approach: clarify the spec (what does it do, what props, what variants), sketch the API first, build the simplest working version, then layer accessibility, keyboard support, controlled/uncontrolled modes, and edge cases. Examples: dropdown, modal, tabs, tooltip. Lead with the API; the implementation follows.

8 min read·~30 min to think through

An open prompt. The interview is testing how you scope, sketch an API, and progressively layer concerns.

Step 1 — clarify the spec

Don't dive in. Ask:

  • What component? (Dropdown, modal, tabs, tooltip — let them name it.)
  • What props matter? (controlled vs uncontrolled, default state, callbacks.)
  • Variants? (sizes, colors, with/without close button.)
  • Accessibility expected? (ARIA, keyboard, focus trap.)
  • Composition style? (compound vs single-component-with-props.)

Step 2 — sketch the API first

Before writing the body, write the call site.

tsx
// Compound (Radix-like)
<Dropdown>
  <Dropdown.Trigger>Open</Dropdown.Trigger>
  <Dropdown.Menu>
    <Dropdown.Item onSelect={save}>Save</Dropdown.Item>
    <Dropdown.Item onSelect={del}>Delete</Dropdown.Item>
  </Dropdown.Menu>
</Dropdown>

// Single-component
<Dropdown
  trigger={<button>Open</button>}
  items={[
    { label: 'Save', onSelect: save },
    { label: 'Delete', onSelect: del },
  ]}
/>

Compound is more flexible (custom triggers and items); single-component is simpler.

Step 3 — implement the minimum

For a dropdown:

tsx
function Dropdown({ trigger, items }: Props) {
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const onClick = (e: MouseEvent) => {
      if (!ref.current?.contains(e.target as Node)) setOpen(false);
    };
    if (open) document.addEventListener('mousedown', onClick);
    return () => document.removeEventListener('mousedown', onClick);
  }, [open]);

  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button onClick={() => setOpen(o => !o)}>{trigger}</button>
      {open && (
        <ul role="menu">
          {items.map(it => (
            <li key={it.label} role="menuitem">
              <button onClick={() => { it.onSelect(); setOpen(false); }}>
                {it.label}
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Step 4 — layer concerns

Keyboard: Esc closes, ArrowDown moves focus, Enter selects.

tsx
useEffect(() => {
  const onKey = (e: KeyboardEvent) => {
    if (e.key === 'Escape') setOpen(false);
    // ... arrow keys
  };
  if (open) document.addEventListener('keydown', onKey);
  return () => document.removeEventListener('keydown', onKey);
}, [open]);

Controlled mode:

tsx
function Dropdown({ open: openProp, onOpenChange, ...rest }: Props) {
  const [internalOpen, setInternalOpen] = useState(false);
  const isControlled = openProp !== undefined;
  const open = isControlled ? openProp : internalOpen;
  const setOpen = isControlled ? onOpenChange : setInternalOpen;
  // ...
}

ARIA:

  • Trigger: aria-haspopup="menu", aria-expanded={open}.
  • Menu: role="menu", aria-labelledby={triggerId}.
  • Items: role="menuitem".

Portal: render menu in a portal to escape overflow: hidden ancestors.

Positioning: use floating-ui (@floating-ui/react) for collision-aware placement.

Step 5 — testing

tsx
test('opens on click and closes on outside click', async () => {
  render(<Dropdown trigger="Open" items={[{ label: 'Save' }]} />);
  await userEvent.click(screen.getByRole('button', { name: 'Open' }));
  expect(screen.getByRole('menu')).toBeInTheDocument();
  await userEvent.click(document.body);
  expect(screen.queryByRole('menu')).not.toBeInTheDocument();
});

What interviewers look for

  1. Clarifying questions before writing code.
  2. API design that's flexible without being overengineered.
  3. A11y mention — even if you don't implement everything, name what you'd add.
  4. Controlled/uncontrolled awareness — design for both.
  5. Composability — compound components > 100-prop single component.
  6. State machine awareness for non-trivial UI (combobox, datepicker).

Reference implementations

Radix UI, Headless UI, Ark UI, React Aria. All open source — read their source for production patterns.

Follow-up questions

  • How do you handle controlled vs uncontrolled state in a single component?
  • When would you reach for a portal?
  • What ARIA attributes are required for a menu?

Common mistakes

  • Diving into code before clarifying the spec.
  • Skipping a11y and keyboard — interviewers notice.
  • Overengineering the API with too many props.

Performance considerations

  • Most UI components are tiny. Optimize only when measured — typically only the menu list itself, if very long. Memoize items if you're passing an array prop that's recreated each parent render.

Edge cases

  • Menu overlapping screen edge — needs floating-ui or manual collision detection.
  • Nested menus — focus management gets harder.
  • Touch devices — outside-click via mousedown alone misses touch.

Real-world examples

  • Radix UI / Headless UI / Ark UI / shadcn/ui — production-grade implementations of every common UI primitive. They handle ARIA, focus, RTL, animation, portal, controlled/uncontrolled, polymorphic types.

Senior engineer discussion

Senior signal: lead with the API, treat accessibility as non-negotiable, and know when to reach for a library. Building a real production dropdown is ~500 lines once you handle a11y, focus, positioning, RTL. Be honest about that.

Related questions