Skip to content

Modal

Modal is for focused decisions and blocking workflows. It should reduce the current page to one clear task, then return users to where they were.

Modal basic interaction Open in Storybook

When To Use

Use Modal for confirmations, short forms, permissions, or decisions that should interrupt the current workflow. Do not use it for large multi-page flows or content users need to compare with the page behind it.

tsx
import { Button, Modal, Portal } from '@vellira-ui/react';

<Modal open={open} onOpenChange={setOpen}>
  <Modal.Trigger asChild>
    <Button>Invite teammate</Button>
  </Modal.Trigger>

  <Portal>
    <Modal.Overlay />
    <Modal.Content>
      <Modal.Header>
        <div>
          <Modal.Title>Invite teammate</Modal.Title>
          <Modal.Description>
            Send an invitation to join this workspace.
          </Modal.Description>
        </div>
        <Modal.Close />
      </Modal.Header>
      <Modal.Body>Choose a role and send the invite.</Modal.Body>
      <Modal.Footer>
        <Modal.Close asChild>
          <Button appearance='ghost' color='neutral'>
            Cancel
          </Button>
        </Modal.Close>
        <Button>Send invite</Button>
      </Modal.Footer>
    </Modal.Content>
  </Portal>
</Modal>;

Destructive Confirmation

Irreversible actions should use plain language, a danger action, and a safe secondary path.

tsx
<Modal
  open={confirmingDelete}
  onOpenChange={setConfirmingDelete}
  role='alertdialog'
>
  <Portal>
    <Modal.Overlay />
    <Modal.Content>
      <Modal.Header>
        <div>
          <Modal.Title>Delete workspace?</Modal.Title>
          <Modal.Description>
            This permanently deletes projects, members, and billing history.
          </Modal.Description>
        </div>
        <Modal.Close />
      </Modal.Header>
      <Modal.Body>This action cannot be undone.</Modal.Body>
      <Modal.Footer>
        <Modal.Close asChild>
          <Button appearance='ghost' color='neutral' disabled={deleting}>
            Cancel
          </Button>
        </Modal.Close>
        <Button
          color='danger'
          loading={deleting}
          loadingText='Deleting...'
          onClick={deleteWorkspace}
        >
          Delete workspace
        </Button>
      </Modal.Footer>
    </Modal.Content>
  </Portal>
</Modal>

Real Example: Invite Teammate

tsx
import { Button, Input, Modal, Portal, Select } from '@vellira-ui/react';
import { useState } from 'react';

export function InviteTeammateDialog() {
  const [open, setOpen] = useState(false);
  const [email, setEmail] = useState('');
  const [role, setRole] = useState('editor');
  const [sending, setSending] = useState(false);

  return (
    <Modal open={open} onOpenChange={setOpen}>
      <Modal.Trigger asChild>
        <Button>Invite teammate</Button>
      </Modal.Trigger>

      <Portal>
        <Modal.Overlay />
        <Modal.Content>
          <Modal.Header>
            <div>
              <Modal.Title>Invite teammate</Modal.Title>
              <Modal.Description>
                Send an invitation to join this workspace.
              </Modal.Description>
            </div>
            <Modal.Close />
          </Modal.Header>
          <Modal.Body>
            <Input
              label='Email'
              type='email'
              value={email}
              onValueChange={setEmail}
            />
            <Select label='Role' value={role} onValueChange={setRole}>
              <Select.Item value='admin'>Admin</Select.Item>
              <Select.Item value='editor'>Editor</Select.Item>
              <Select.Item value='viewer'>Viewer</Select.Item>
            </Select>
          </Modal.Body>
          <Modal.Footer>
            <Modal.Close asChild>
              <Button appearance='ghost' color='neutral'>
                Cancel
              </Button>
            </Modal.Close>
            <Button
              loading={sending}
              loadingText='Sending invite...'
              onClick={() => setSending(true)}
            >
              Send invite
            </Button>
          </Modal.Footer>
        </Modal.Content>
      </Portal>
    </Modal>
  );
}

Anatomy

PartGuidance
TriggerOpens the dialog through Modal.Trigger.
PortalRenders overlay layers through the shared Portal primitive.
OverlayDims the page and can close when outside press is enabled.
HeaderGroups Modal.Title, Modal.Description, and close affordance.
BodyExplain consequence, scope, or required input.
FooterPut the safe action and primary action in a predictable order.
CloseUse Modal.Close for cancel and icon close controls.

Styling

Modal uses component tokens for overlay color, content surface, border, radius, shadow, spacing, and close-button states. Prefer theme tokens for custom form controls inside modal bodies so light, dark, and high-contrast themes stay in sync.

Default motion is driven by modal.motion tokens: backdrop opacity runs for 180ms on open and 150ms on close; content animates opacity, scale, and translateY over the same timings. Use animation='slide', animation='fade', or animation='none' when the product surface needs a different motion profile. duration accepts either a number or { open, close }, and easing='standard' uses the tokenized default curve.

Accessibility

  • Provide a visible Modal.Title or pass ariaLabel to Modal.Content.
  • Use Modal.Description for descriptive content, especially alertdialog.
  • Keep at least one obvious close/cancel path with Modal.Close.
  • Disable or show loading on the final action while async work is pending.
  • For destructive flows, do not rely only on color. Use explicit wording.

See Also

Built for Vellira Design System.