Component API design is the contract between your UI component and the engineers who consume it. In frontend interviews, the answer is not just the JSX that renders; it is the React props, events, slots, state ownership, accessibility, styling, and escape hatches you choose not to expose.
Use this as a frontend component API design interview practice map. Start from a familiar component prompt, name the API contract, ship the MVP, then explain the trade-offs that would make the component scale in a team codebase.
Quick answer: how to design a component API
Start by naming the consumer and the state owner. Then define the props/events, composition model, accessibility contract, styling surface, and the escape hatches you are intentionally leaving for follow-up.
- Consumer: who uses the component, and what behavior must they control?
- State ownership: controlled, uncontrolled, or both through value/defaultValue callbacks.
- Props/events: public inputs and domain events with useful payloads.
- Composition: simple props, children, slots, or compound components.
- Accessibility and styling: labels, keyboard paths, focus policy, variants, className, and CSS variables.
- Score signal: clear contract, small MVP, explicit trade-offs, and no unnecessary escape hatches.
How this guide was reviewed
This guide is maintained by FrontendAtlas and reviewed against our UI component drill set, live coding editor checks, interview blueprint scoring rubrics, and official component library patterns. Use it as a timed decision checklist, not as a generic React API pattern list.
Component API decision matrix
- 1Consumer needWho calls this component, and what must they control?
- 2State ownershipParent-owned, local, or both through value/defaultValue.
- 3Composition modelSimple props, children, named slots, or compound components.
- 4Event payloadDomain event with value, item, reason, or context.
- 5A11y and stylingLabels, keyboard behavior, focus policy, variants, and tokens.
- 6Escape hatchRef, asChild, slotProps, or render hook only when needed.
Try the API surface trade-off
Toggle the same button prompt through three API surfaces. The strongest answer is not always the most flexible one; it is the API that matches the consumer and round scope.
<Button>
<DownloadIcon aria-hidden="true" />
Export CSV
</Button>Interviewer signal: The caller owns custom content while the component keeps button semantics and focus behavior.
Risk: Composition needs clear a11y guidance so callers do not pass confusing markup.
Component API design patterns interviewers ask about
These prompts expose different API decisions: controlled state, composition, event payloads, a11y contracts, styling hooks, and when to keep the surface area small.
Modal / Confirm Dialog
Public surface: open, defaultOpen, onOpenChange, title, description, actions.
Trade-off/test focus: controlled vs uncontrolled, focus restore, Escape policy, slots.
Tabs
State model: value, defaultValue, onValueChange, item ids, disabled tabs.
Trade-off/test focus: roving tabindex, tabpanel linkage, controlled active value.
Accordion
State model: type, value, defaultValue, collapsible, onValueChange.
Trade-off/test focus: single vs multiple sections, button semantics, disabled items.
Autocomplete / Combobox
Consumer API: inputValue, selectedValue, onInputChange, onSelect, renderOption.
Trade-off/test focus: async states, stale responses, keyboard selection, option identity.
Contact Form
Props/events: field schema, validation timing, onSubmit(values), errors, disabled submit.
Trade-off/test focus: controlled fields, accessible errors, submit lifecycle.
Data Table / Pagination
Consumer API: rows, columns, page, pageSize, onPageChange, empty state.
Trade-off/test focus: derived rows, page bounds, cell renderers, table semantics.
Dynamic Table
Public surface: column descriptors, accessors, header labels, fallback renderers, stable row keys.
Trade-off/test focus: typed columns, missing values, sort follow-up, prop explosion.
Nested Checkbox Tree
State model: nodes, selected ids, onCheckedChange(ids), indeterminate state, disabled nodes.
Trade-off/test focus: parent-child sync, controlled selection, reset, traversal cost.
Star Rating
A11y contract: value, max, readOnly, onValueChange, labels.
Trade-off/test focus: hover preview vs committed value, keyboard input, a11y names.
Progress Bar
A11y contract: value, min, max, threshold colors, label, reduced motion.
Trade-off/test focus: bounded values, ARIA progressbar, theme hooks, animation policy.
Component API decision framework
Controlled vs uncontrolled
Name state ownership first: value/defaultValue/onValueChange for value state and open/defaultOpen/onOpenChange for visibility state.
- Controlled when parent coordinates validation, URL state, analytics, or reset.
- Uncontrolled when the widget is isolated and interview time is tight.
- Decision tree: if another component needs the state, lift it; otherwise keep it local and expose a controlled mode as follow-up.
Composition vs configuration
Prefer children, named slots, or compound components when content shape changes; use simple props when the API is stable.
- Prop explosion is a smell when every new layout needs another boolean.
- Compound components work well for Modal, Tabs, Accordion, and Menu prompts.
- Configuration is fine for small variants like
sizeandvariant.
Event naming and payloads
Use domain events instead of raw DOM events: onSelect({ value, item, reason }) is easier to consume and test than leaking click details.
- Name events by state change:
onValueChange,onOpenChange,onPageChange. - Include enough payload for analytics, undo, validation, or async follow-up.
- Keep the raw event optional unless the interviewer asks for it.
Styling surface
Expose variant, size, className, and CSS variables when they map to real design-system needs.
- Use enums for known visual choices; avoid many boolean styling props.
- Expose CSS variables for theme values like colors, radius, and spacing.
- Do not make every internal element individually styleable in the first pass.
Escape hatches
Mention ref, asChild, slotProps, and render hooks as follow-ups, not as the first API surface.
- Use
reffor focus and integration with forms or popovers. - Use
slotPropsonly when consumers need targeted internal element control. - Avoid over-abstracted state reducers unless the prompt asks for library-grade APIs.
Accessibility as API
Treat labels, ids, ARIA linkage, keyboard expectations, focus restore, and focus trap as part of the public contract.
- Expose labels and descriptions instead of hardcoding anonymous controls.
- State keyboard behavior before coding: Escape, Arrow keys, Enter, Tab order.
- Call out focus restore/trap policy for dialogs, popovers, and menus.
Weak vs strong component API
Interviewers can usually tell whether you are designing from the consumer's point of view. A weak API adds one prop for every request. A stronger API names ownership, gives consumers a composable slot, and keeps events meaningful.
Practice note from FrontendAtlas drills: in timed component API drills, weak answers often add one prop per requested variation. Stronger answers first name state ownership, composition boundaries, and event payloads, then keep the initial API small enough to implement.
| Prompt | Weak API smell | Stronger API move | Interview signal |
|---|---|---|---|
| Button icon | iconName, iconSize, iconColor, onIconHover. | Accept icon, renderIcon, or a named slot so the caller owns custom icon behavior. | You avoid prop explosion and let composition handle flexible content. |
| Modal actions | primaryText, secondaryText, danger, and many footer booleans. | Expose header/body/footer slots plus open/defaultOpen/onOpenChange for state ownership. | You separate structure from state and keep destructive flows controllable. |
| Data Table columns | Hardcoded field names, hidden sort rules, and one-off formatting props. | Use column descriptors with accessors, headers, fallback renderers, and stable row keys. | You make the API reusable without turning it into a generic grid library. |
TypeScript API guardrails
TypeScript should make invalid component states harder to express, not make the interview answer feel like a type puzzle. Use types to protect the public surface, then move back to behavior, accessibility, and trade-offs.
| Guardrail | Use it when | Example shape | Interview caution |
|---|---|---|---|
| Mirror native props | A component wraps a real DOM element like button, input, or anchor. | ComponentPropsWithoutRef<'button'> plus your own variants. | Omit conflicting props before adding custom names like size or variant. |
| Controlled/uncontrolled union | The component supports both parent-owned and local state. | { value; onValueChange } | { defaultValue? }. | Explain the contract; do not spend the whole round perfecting utility types. |
| Mutually exclusive props | Two props should not be valid together. | { href: string; onClick?: never } | { onClick: () => void; href?: never }. | Use this for important invalid states, not for every minor visual option. |
| Discriminated unions | Variants have different required payloads. | { type: 'link'; href } | { type: 'button'; onClick }. | Name the runtime behavior each branch changes. |
| Ref forwarding | Consumers need focus, measurement, forms, popovers, or composition. | forwardRef<HTMLButtonElement, ButtonProps>. | Mention it as an integration point, then return to the core prompt. |
Library patterns interviewers recognize
You do not need to copy a library API in an interview, but referencing familiar patterns shows that your design choices match real component systems.
The official docs below are references for the pattern vocabulary; adapt the pattern to the prompt, consumer, and timebox instead of copying a full library surface.
| Pattern | What it solves | Risk | Interview takeaway |
|---|---|---|---|
| React controlled/uncontrolled | Lets the caller choose parent-owned state or simple local state. | Supporting both can double edge cases if naming is inconsistent. | Use value/defaultValue/onValueChange and state who owns the source of truth. |
Radix asChild | Composes primitive behavior onto a caller-provided element. | The child must spread props, forward refs, and remain accessible. | Great follow-up for design systems, not the first API for a small interview MVP. |
MUI slotProps | Gives targeted control over internal elements without many top-level props. | Can expose too much internal structure too early. | Use when consumers need stable subpart customization. |
| React Aria hooks | Encodes accessibility behavior while you own rendering. | Still requires correct labels, relationships, and state wiring. | Separate behavior contracts from visual composition. |
| Headless UI render props | Exposes state like active, selected, open, or disabled to custom markup. | Render props can get noisy when simple children would be enough. | Use when visual control matters more than fixed markup. |
Worked examples: API contracts interviewers can score
Modal/Dialog API
Contract: open/defaultOpen/onOpenChange, title, description, slots for header/body/footer, focus restore, Escape policy, and an explicit a11y contract.
- Use controlled open state when parent owns destructive action flow.
- Use slots when footer actions vary across confirm, alert, and custom dialogs.
- Practice: Modal / Confirm Dialog.
Tabs API
Contract: controlled active value, item ids, onValueChange, disabled tab policy, roving tabindex, and linked tabpanel ids.
- Use stable string ids instead of array indexes when panels can reorder.
- Clarify whether disabled tabs are skipped by Arrow keys.
- Practice: React Tabs.
Autocomplete API
Contract: inputValue vs selected value, onInputChange, onSelect, async loading/error/empty states, stale response policy, and renderOption for custom rows.
- Separate typed text from committed selection so clearing and editing are predictable.
- Ignore stale responses or track request identity before rendering options.
- Practice: React Autocomplete.
Minimal example: these two types show the difference between state ownership and domain event payloads. In an interview, write only enough shape to make the contract testable before implementing behavior.
type DialogProps = {
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean, reason: 'trigger' | 'escape' | 'confirm' | 'cancel') => void;
title: string;
description?: string;
children: React.ReactNode;
};
type SelectEvent<T> = {
value: string;
item: T;
reason: 'keyboard' | 'pointer' | 'programmatic';
};45/60-minute component API round flow
- Spend the first minutes clarifying consumers and state ownership.
- Define the public surface before coding.
- Use the final minutes to explain trade-offs and skipped escape hatches.
| Time | Move | Signal |
|---|---|---|
| 0-5 min | Clarify consumers, data shape, ownership, keyboard expectations, and required customization. | You design for the caller, not just the demo. |
| 5-12 min | Define contract: props, events, slots, state ownership, accessibility labels, and styling surface. | Your API is explainable before implementation. |
| 12-30 min | Ship MVP behavior with the smallest API that supports the prompt. | You can implement without over-designing. |
| 30-45 min | Add a11y/keyboard, event payloads, controlled mode, disabled/error/loading states. | You know frontend correctness lives in contracts. |
| 45-60 min | Explain trade-offs, name follow-ups, and identify what you intentionally did not expose. | You can reason like a component owner. |
What interviewers score
- Clear contract: props, events, slots, and ownership are named before complexity grows.
- State ownership: controlled/uncontrolled decisions match the caller and prompt constraints.
- Event payloads: domain payloads carry useful data and reasons without leaking internals.
- A11y contract: labels, ids, ARIA linkage, keyboard paths, and focus behavior are explicit.
- Styling surface: variants, size, className, and CSS variables are enough without exposing everything.
- Trade-off narration: you can name the API you skipped and why it was not needed yet.
What to skip vs prioritize
- Prioritize the contract, a11y, state ownership, event payloads, and one dry run.
- Save compound components, render hooks, refs, and slot props for follow-up depth.
- Skip theme engines, virtualization, global config, and library-grade APIs unless asked.
| Priority | Do this | Avoid spending time on |
|---|---|---|
| Prioritize | Clear contract, state ownership, event payload, a11y, styling surface, and a dry run. | Inventing a generic component library before the prompt works. |
| Know lightly | Compound components, slot props, render hooks, refs, and escape hatches as follow-ups. | Over-abstracted state reducers in the first implementation pass. |
| Skip unless asked | Theme engines, polymorphic components, portals, virtualization, and global config systems. | Excessive escape hatches that make the API harder to explain. |
Choose your next practice format
Component API design FAQ
What is component API design?
Component API design is the way you define how another engineer consumes your component: props, events, slots, state ownership, accessibility labels, styling hooks, and escape hatches.
How do I practice component API design for frontend interviews?
Pick a real prompt like Modal, Tabs, Autocomplete, or Data Table. Name the API contract first, then implement the MVP, keyboard support, a11y contract, styling surface, and trade-offs.
What is the controlled vs uncontrolled component API pattern?
Say who owns the state. Controlled components receive state and callbacks from the parent; uncontrolled components manage local state but can expose defaults and change events.
When should I use compound components or slots?
Use compound components or slots when the content shape varies, such as Modal, Tabs, Accordion, Menu, or Table prompts. Use simple props for stable, small choices.
How should component APIs expose events and accessibility?
Prefer domain payloads like onSelect({ value, item, reason }) instead of raw DOM events. Accessibility is part of the public API too: labels, ids, ARIA relationships, keyboard behavior, visible focus, and focus restore should be named in the contract.
How should TypeScript shape a component API?
TypeScript should make invalid component states harder to express. Use native prop mirroring, controlled/uncontrolled unions, mutually exclusive props, discriminated unions, and ref forwarding only where they clarify the public contract.
Which real library patterns help in component API interviews?
Useful patterns include React controlled/uncontrolled state, Radix asChild composition, MUI slotProps, React Aria hooks, and Headless UI render props. Use them as reference points, not as APIs to copy blindly.
How is this component API design guide reviewed?
FrontendAtlas reviews this guide against UI component drills, live coding editor checks, and interview blueprint scoring rubrics so the advice stays tied to timed interview execution.
How do I choose between prop-heavy, composed, and controlled component APIs?
Use a prop-heavy API only for a small stable surface, use composition when callers need custom content, and use controlled state when a parent must coordinate validation, analytics, reset, loading, or URL state.