Lumen Docs

Modal

Modal primitive. Located at apps/web/components/ui/modal.tsx. Composed of four parts.

Usage

<Modal open={open} onClose={onClose} className="max-w-[520px]">
  <ModalHeader>Add member</ModalHeader>
  <ModalBody className="flex flex-col gap-4">
    <Input label="Full name" ... />
    <Input label="Email" ... />
  </ModalBody>
  <ModalFooter>
    <Button variant="ghost" onClick={onClose}>Cancel</Button>
    <Button variant="primary" onClick={handleSubmit}>Create</Button>
  </ModalFooter>
</Modal>

Size presets

By default, modal is max-w-[480px]. Override via className:

| Size | Use | |---|---| | max-w-[400px] | Simple confirm dialogs | | max-w-[520px] | Forms (add member, invite) | | max-w-[620px] | Multi-field settings | | max-w-[820px] | Side-sidebar layouts (Project Settings) |

Height: auto-fit by default. For multi-section modals, add h-[600px] max-h-[85vh] flex flex-col to the Modal className and make each section flex-1 min-h-0 overflow-auto.

ModalHeader

<ModalHeader>Add member</ModalHeader>

Renders px-6 pt-6 pb-2 text-base font-semibold. Close X in the top-right is automatic.

ModalBody

<ModalBody className="flex flex-col gap-4">
  {/* form fields */}
</ModalBody>

Default padding px-6 pb-4. Override via className if needed (e.g., dense tables).

ModalFooter

<ModalFooter>
  <Button variant="ghost" onClick={onClose}>Cancel</Button>
  <Button variant="primary" onClick={save}>Save</Button>
</ModalFooter>

Primary action on the right. Cancel on the left (ghost variant). Separator line above.

Error state

Alert banner goes inside ModalBody at the top:

<ModalBody className="flex flex-col gap-4">
  {error && (
    <div className="px-3 py-2 text-sm text-error bg-error/10 border border-error/20 rounded-[var(--radius-sm)]">
      {error}
    </div>
  )}
  {/* fields */}
</ModalBody>

Closing behavior

  • Click backdrop → fires onClose
  • Press Escape → fires onClose
  • Click close X → fires onClose

Modal does NOT reset internal state on close — the parent owns that. When you pass in fresh state each time, clear it via a reset() function.

Don't

  • Don't pass onClose={() => setOpen(false)} directly inside — use a handleClose that also resets form state if needed
  • Don't nest modals — use a second screen within the same modal (e.g., step state) instead
  • Don't add custom close behavior (disable backdrop click etc.) without reason — users expect standard dismiss