CodePark/ questions / Component architecture & APIs
START PRACTICINGPRACTICE
Frontend question · Frontend system design › Component architecture & APIs

Design the API of a reusable Modal component for a design system. What props does it take, and how do you handle open state, focus and accessibility?

<Modal open={open} onOpenChange={setOpen}>
  <Modal.Trigger asChild><Button>Edit</Button></Modal.Trigger>
  <Modal.Content aria-describedby="edit-desc">
    <Modal.Title>Edit profile</Modal.Title>
    <p id="edit-desc">Changes are saved immediately.</p>
    <ProfileForm />
    <Modal.Close asChild><Button>Done</Button></Modal.Close>
  </Modal.Content>
</Modal>

Use a compound component API. This means the modal is a set of small parts (Modal.Trigger, Modal.Content, Modal.Title, Modal.Close) that share state through the parent. Consumers arrange the layout, and the modal owns the behaviour.

Open state supports two modes:

  • Controlled: the parent passes open and onOpenChange and owns the state.
  • Uncontrolled: the parent passes defaultOpen and uses the built-in trigger. The modal owns the state.

Accessibility is the core of the component, not an option. It works like this:

  • Content renders in a portal with role="dialog" and aria-modal. A portal renders the content outside the normal DOM tree, usually at the end of body.
  • Modal.Title is connected to aria-labelledby automatically.
  • Focus moves into the dialog when it opens.
  • Focus is trapped inside while it is open.
  • Focus returns to the trigger when it closes.

Escape and a backdrop click both close it. Each can be overridden through props like onEscapeKeyDown, for cases such as unsaved changes. The page behind gets scroll lock and is made inert, which means users cannot click or focus it.

I keep styling open through className and design tokens (named values for colours and spacing), rather than many visual props. The native dialog element with showModal() now handles much of the focus and inert behaviour. It is worth building on.

  • Compound components give consumers flexibility over the layout.
  • Open state works both controlled and uncontrolled.
  • Focus is trapped while open and returns to the trigger on close. Escape closes it, and aria-labelledby names it.
  • Content renders in a portal, with scroll lock and an inert background.
  • Props such as onEscapeKeyDown act as escape hatches, so consumers can change the closing behaviour.