# Modal

Display popup content that requires attention or provides additional information.

---

## Default

```tsx
import {
  Button,
  Modal,
  ModalBody,
  ModalHeader,
  ModalTitle,
  ModalSubtitle,
  ModalActions,
  ModalAction,
} from '@vercel/geistcn/components';
import { useState, type JSX } from 'react';

export function Component(): JSX.Element {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button onClick={() => setOpen(true)} size="small">
        Open Modal
      </Button>

      <Modal active={open} onClickOutside={() => setOpen(false)}>
        <ModalBody>
          <ModalHeader>
            <ModalTitle>Create Token</ModalTitle>
            <ModalSubtitle>
              Enter a unique name for your token to differentiate it from other
              tokens and then select the scope.
            </ModalSubtitle>
          </ModalHeader>

          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
        </ModalBody>

        <ModalActions>
          <ModalAction onClick={() => setOpen(false)} variant="secondary">
            Cancel
          </ModalAction>

          <ModalAction onClick={() => setOpen(false)}>Submit</ModalAction>
        </ModalActions>
      </Modal>
    </>
  );
}
```

## Sticky

```tsx
import {
  Button,
  Modal,
  ModalBody,
  ModalHeader,
  ModalTitle,
  ModalActions,
  ModalAction,
} from '@vercel/geistcn/components';
import { ArrowLeft } from '@vercel/geistcn/icons';
import { useState, type JSX } from 'react';

export function Component(): JSX.Element {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button onClick={() => setOpen(true)} size="small">
        Open Modal
      </Button>

      <Modal active={open} onClickOutside={() => setOpen(false)} sticky>
        <ModalBody>
          <ModalHeader>
            <ModalTitle>Create Token</ModalTitle>
          </ModalHeader>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
        </ModalBody>

        <ModalActions>
          <div>
            <ModalAction onClick={() => setOpen(false)} variant="secondary">
              Cancel
            </ModalAction>
            <ModalAction
              onClick={() => setOpen(false)}
              prefix={<ArrowLeft />}
              variant="secondary"
            >
              Previous
            </ModalAction>
          </div>

          <ModalAction onClick={() => setOpen(false)}>Submit</ModalAction>
        </ModalActions>
      </Modal>
    </>
  );
}
```

## Single button

```tsx
import {
  Button,
  Modal,
  ModalBody,
  ModalHeader,
  ModalTitle,
  ModalActions,
  ModalAction,
} from '@vercel/geistcn/components';
import { useState, type JSX } from 'react';

export function Component(): JSX.Element {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button onClick={() => setOpen(true)} size="small">
        Open Modal
      </Button>

      <Modal active={open} onClickOutside={() => setOpen(false)} sticky>
        <ModalBody>
          <ModalHeader>
            <ModalTitle>Create Token</ModalTitle>
          </ModalHeader>
          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
        </ModalBody>
        <ModalActions>
          <ModalAction fullWidth onClick={() => setOpen(false)}>
            Cancel
          </ModalAction>
        </ModalActions>
      </Modal>
    </>
  );
}
```

## Disabled actions

```tsx
import {
  Button,
  Modal,
  ModalBody,
  ModalHeader,
  ModalTitle,
  ModalSubtitle,
  ModalActions,
  ModalAction,
} from '@vercel/geistcn/components';
import { useState, type JSX } from 'react';

export function Component(): JSX.Element {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button onClick={() => setOpen(true)} size="small">
        Open Modal
      </Button>

      <Modal active={open} onClickOutside={() => setOpen(false)}>
        <ModalBody>
          <ModalHeader>
            <ModalTitle>Modal</ModalTitle>
            <ModalSubtitle>This is a modal.</ModalSubtitle>
          </ModalHeader>

          <p className="text-copy-14">
            Some content contained within the modal.
          </p>
        </ModalBody>

        <ModalActions>
          <ModalAction onClick={() => setOpen(false)} variant="secondary">
            Cancel
          </ModalAction>

          <ModalAction disabled onClick={() => setOpen(false)}>
            Submit
          </ModalAction>
        </ModalActions>
      </Modal>
    </>
  );
}
```

## Inset

```tsx
import {
  Button,
  Modal,
  ModalBody,
  ModalHeader,
  ModalTitle,
  ModalSubtitle,
  ModalInset,
  ModalActions,
  ModalAction,
} from '@vercel/geistcn/components';
import { useState, type JSX } from 'react';

export function Component(): JSX.Element {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button onClick={() => setOpen(true)} size="small">
        Open Modal
      </Button>

      <Modal active={open} onClickOutside={() => setOpen(false)}>
        <ModalBody>
          <ModalHeader>
            <ModalTitle>Modal</ModalTitle>
            <ModalSubtitle>This is a modal.</ModalSubtitle>
          </ModalHeader>

          <ModalInset>
            <p className="text-copy-14">Content within the inset.</p>
          </ModalInset>

          <div className="pt-6">
            <p className="text-copy-14">Content outside the inset.</p>
          </div>
        </ModalBody>

        <ModalActions>
          <ModalAction onClick={() => setOpen(false)} variant="secondary">
            Cancel
          </ModalAction>

          <ModalAction onClick={() => setOpen(false)}>Submit</ModalAction>
        </ModalActions>
      </Modal>
    </>
  );
}
```

## Control initial focus

```tsx
import {
  Button,
  Modal,
  ModalBody,
  ModalHeader,
  ModalTitle,
  ModalSubtitle,
  ModalActions,
  ModalAction,
} from '@vercel/geistcn/components';
import { useRef, useState, type JSX } from 'react';

export function Component(): JSX.Element {
  const [open, setOpen] = useState(false);
  const initialFocusRef = useRef<HTMLButtonElement>(null);

  return (
    <>
      <Button onClick={() => setOpen(true)} size="small">
        Open Modal
      </Button>

      <Modal
        active={open}
        initialFocusRef={initialFocusRef}
        onClickOutside={() => setOpen(false)}
      >
        <ModalBody>
          <ModalHeader>
            <ModalTitle>Initial Focus</ModalTitle>
            <ModalSubtitle>
              This Modal is set up to programmatically move the focus onto the
              Submit button, making it possible to promptly continue with the
              Enter key.
            </ModalSubtitle>
          </ModalHeader>
        </ModalBody>

        <ModalActions>
          <ModalAction onClick={() => setOpen(false)} variant="secondary">
            Cancel
          </ModalAction>

          <ModalAction onClick={() => setOpen(false)} ref={initialFocusRef}>
            Submit
          </ModalAction>
        </ModalActions>
      </Modal>
    </>
  );
}
```

## Focus an input on open

```tsx
import {
  Button,
  Input,
  Modal,
  ModalBody,
  ModalHeader,
  ModalTitle,
  ModalSubtitle,
  ModalActions,
  ModalAction,
} from '@vercel/geistcn/components';
import { useRef, useState, type JSX } from 'react';

export function Component(): JSX.Element {
  const [open, setOpen] = useState(false);
  const [name, setName] = useState('');
  const initialFocusRef = useRef<HTMLInputElement>(null);

  return (
    <>
      <Button onClick={() => setOpen(true)} size="small">
        Open Modal
      </Button>

      <Modal
        active={open}
        initialFocusRef={initialFocusRef}
        onClickOutside={() => setOpen(false)}
      >
        <ModalBody>
          <ModalHeader>
            <ModalTitle>Invite Member</ModalTitle>
            <ModalSubtitle>
              On both desktop and the mobile bottom sheet, the Name field
              receives focus when the Modal opens so the user can start typing
              immediately.
            </ModalSubtitle>
          </ModalHeader>

          <div className="flex flex-col gap-3">
            <Input
              aria-labelledby="modal-initial-focus-input-name"
              label="Name"
              onChange={(e) => setName(e.target.value)}
              placeholder="Jane Doe"
              ref={initialFocusRef}
              value={name}
            />
          </div>
        </ModalBody>

        <ModalActions>
          <ModalAction onClick={() => setOpen(false)} variant="secondary">
            Cancel
          </ModalAction>

          <ModalAction onClick={() => setOpen(false)}>Send Invite</ModalAction>
        </ModalActions>
      </Modal>
    </>
  );
}
```

## Mobile sheet with inputs

```tsx
import {
  Button,
  Input,
  Modal,
  ModalBody,
  ModalHeader,
  ModalTitle,
  ModalSubtitle,
  ModalActions,
  ModalAction,
} from '@vercel/geistcn/components';
import { useState, type JSX } from 'react';

export function Component(): JSX.Element {
  const [open, setOpen] = useState(false);
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  return (
    <>
      <Button onClick={() => setOpen(true)} size="small">
        Open Modal
      </Button>

      <Modal active={open} onClickOutside={() => setOpen(false)}>
        <ModalBody>
          <ModalHeader>
            <ModalTitle>Invite Member</ModalTitle>
            <ModalSubtitle>
              On a mobile viewport this opens as a bottom sheet. Verify that
              both inputs receive focus and accept keyboard input.
            </ModalSubtitle>
          </ModalHeader>

          <div className="flex flex-col gap-3">
            <Input
              aria-labelledby="modal-mobile-inputs-name"
              label="Name"
              onChange={(e) => setName(e.target.value)}
              placeholder="Jane Doe"
              value={name}
            />
            <Input
              aria-labelledby="modal-mobile-inputs-email"
              label="Email"
              onChange={(e) => setEmail(e.target.value)}
              placeholder="jane@example.com"
              value={email}
            />
          </div>
        </ModalBody>

        <ModalActions>
          <ModalAction onClick={() => setOpen(false)} variant="secondary">
            Cancel
          </ModalAction>

          <ModalAction onClick={() => setOpen(false)}>Send Invite</ModalAction>
        </ModalActions>
      </Modal>
    </>
  );
}
```

## Best Practices

### When to use

* Use Modal when a decision must block the rest of the page. For persistent associated context where the underlying page stays readable, use `Sheet` on desktop or `Drawer` on mobile.
* Confirm destructive actions in a Modal. `Drawer` and `Sheet` don’t fully dim the page, so they read as too soft for a delete or revoke.
* Skip Modal for routine create flows that have a dedicated page; route to the page instead.

### Behavior

* Default focus to `Cancel` on any destructive Modal. Enter must never trigger the destructive action without a typed confirmation.
* Allow Escape and outside-click to dismiss non-destructive Modals; gate dismissal on destructive ones with unsaved input.
* Trap focus inside the Modal while it’s open and return focus to the trigger after close. Restore body scroll on the same tick the Modal unmounts.
* For high-stakes destructive actions (delete production resource, rotate signing key, downgrade plan), gate the primary button on a typed match of the resource name.

### Content

* `Modal.Title` is a Title Case statement, never a question. `Transfer Project` is correct; `Transfer Project?` is wrong.
* `Modal.P` body is sentence case, 1–3 sentences. State the consequence first, then any cascade.
* Primary button is `Verb + Noun` and matches the title verb (`Transfer Project` title pairs with `Transfer Project` button). Never `Confirm`, `OK`, or a bare verb on a destructive primary.
* Cancel literal stays `Cancel`. Acknowledgment-only Modals (after a key reveal, after a one-time-show) use `Done`, never `OK` or `Close`.
* Close irreversible bodies with `This cannot be undone.`; close cascade-only bodies with `Some effects cannot be undone.`. Don’t claim full irreversibility for a partial cascade.
* Pair the success toast verb 1:1 with the primary button: `Delete Project` button, `Project deleted` toast.

### Accessibility

* Set `aria-labelledby` to the `Modal.Title` id so screen readers announce the title on open.
* Keep the Cancel button literally `Cancel` so screen-reader users hear a stable dismissal label across destructive flows.
* After an error inside the Modal, keep focus inside so the user can retry; after success, return focus to the trigger.
