# Modal recipes

> Six ready-made modals on the Dialog primitive: type-to-confirm delete, create project, success with a self-drawing check, keyboard shortcuts, upgrade to Pro and invite teammates with email chips.

- Collection: Blocks
- Tier: pro
- Docs: https://geturui.com/docs/modal-recipes
- Install: `npx shadcn@latest add @geturui-pro/modal-recipes`
- Dependencies: motion, lucide-react

## Variants

### Confirm delete

Alert dialog that lists what will be lost and arms the destructive button only once the project name is typed exactly.

Install: `npx shadcn@latest add @geturui-pro/modal-recipes-confirm-delete`

```tsx
import { ConfirmDeleteModal } from "@/components/blocks/modal-recipes/confirm-delete";
import { Button } from "@/components/ui/button";

export function DangerZone() {
  return (
    <ConfirmDeleteModal
      projectName="acme-marketing"
      trigger={<Button variant="danger">Delete project</Button>}
      onConfirm={() => fetch("/api/projects/acme-marketing", { method: "DELETE" })}
    />
  );
}
```

| Prop | Type | Default | Description |
|---|---|---|---|
| projectName | `string` | — | Text the user must type to enable Delete. |
| onConfirm | `() => void | Promise<unknown>` | — | Runs on confirm; a promise shows a pending state and the modal closes when it resolves. |
| consequences | `{ icon?: ReactNode; text: string }[]` | — | What will be lost, one line each. |
| trigger | `ReactElement` | — | Element that opens the modal; it receives onClick, aria-haspopup and aria-expanded. |
| open / defaultOpen / onOpenChange | `boolean / boolean / (open: boolean) => void` | — | Controlled or uncontrolled open state. |

### Create project

Form modal with a live URL preview under the name, visibility as native radio cards and a native template select.

Install: `npx shadcn@latest add @geturui-pro/modal-recipes-create-project`

```tsx
import { CreateProjectModal } from "@/components/blocks/modal-recipes/create-project";
import { Button } from "@/components/ui/button";

export function NewProject() {
  return <CreateProjectModal workspace="acme" trigger={<Button>New project</Button>} onCreate={async (p) => console.log(p)} />;
}
```

| Prop | Type | Default | Description |
|---|---|---|---|
| workspace | `string` | "acme" | Handle shown in the URL preview. |
| templates | `{ value: string; label: string }[]` | — | Options for the Start from select. |
| onCreate | `(project: { name; slug; visibility; template }) => void | Promise<unknown>` | — | Runs on submit; a promise shows a pending state. |
| trigger | `ReactElement` | — | Element that opens the modal; it receives onClick, aria-haspopup and aria-expanded. |
| open / defaultOpen / onOpenChange | `boolean / boolean / (open: boolean) => void` | — | Controlled or uncontrolled open state. |

### Success

Centred celebration: the check draws itself as a ring ripples out, then next steps blur in one by one.

Install: `npx shadcn@latest add @geturui-pro/modal-recipes-success`

```tsx
import { SuccessModal } from "@/components/blocks/modal-recipes/success";

export function Deployed({ open, onOpenChange }: { open: boolean; onOpenChange: (o: boolean) => void }) {
  return <SuccessModal open={open} onOpenChange={onOpenChange} title="Your site is live" primaryLabel="Open site" />;
}
```

| Prop | Type | Default | Description |
|---|---|---|---|
| title / description | `string / ReactNode` | — | Confirmation copy. |
| steps | `{ icon?; title; description?; onClick? }[]` | — | Next steps; with onClick they become buttons. |
| primaryLabel / onPrimary | `string / () => void` | "Open site" | Primary action; the modal closes after it runs. |
| trigger | `ReactElement` | — | Element that opens the modal; it receives onClick, aria-haspopup and aria-expanded. |
| open / defaultOpen / onOpenChange | `boolean / boolean / (open: boolean) => void` | — | Controlled or uncontrolled open state. |

### Keyboard shortcuts

Wide sheet of grouped shortcuts with keycaps and a filter box; groups reflow with layout springs and it opens on “?” too.

Install: `npx shadcn@latest add @geturui-pro/modal-recipes-keyboard-shortcuts`

```tsx
import { KeyboardShortcutsModal } from "@/components/blocks/modal-recipes/keyboard-shortcuts";

export function Shortcuts() {
  return (
    <KeyboardShortcutsModal
      groups={[{ title: "General", shortcuts: [{ label: "Search", keys: ["⌘", "K"] }] }]}
    />
  );
}
```

| Prop | Type | Default | Description |
|---|---|---|---|
| groups | `{ title: string; shortcuts: { label: string; keys: string[] }[] }[]` | — | Shortcut groups shown in two columns. |
| listenForQuestionMark | `boolean` | true | Open when ? is pressed outside a text field. |
| trigger | `ReactElement` | — | Element that opens the modal; it receives onClick, aria-haspopup and aria-expanded. |
| open / defaultOpen / onOpenChange | `boolean / boolean / (open: boolean) => void` | — | Controlled or uncontrolled open state. |

### Upgrade to Pro

Ember-accented upsell with a monthly / yearly switch on a gliding pill, a price that blurs between cycles and a feature list.

Install: `npx shadcn@latest add @geturui-pro/modal-recipes-upgrade-to-pro`

```tsx
import { UpgradeToProModal } from "@/components/blocks/modal-recipes/upgrade-to-pro";
import { Button } from "@/components/ui/button";

export function Upsell() {
  return <UpgradeToProModal prices={{ monthly: 15, yearly: 12 }} trigger={<Button variant="pro">Upgrade</Button>} onUpgrade={async (cycle) => {}} />;
}
```

| Prop | Type | Default | Description |
|---|---|---|---|
| prices | `{ monthly: number; yearly: number }` | { monthly: 15, yearly: 12 } | Price per seat per month for each cycle. |
| currency | `string` | "$" | Currency symbol. |
| features | `string[]` | — | What Pro includes. |
| defaultCycle | `"monthly" | "yearly"` | "yearly" | Initially selected billing cycle. |
| onUpgrade | `(cycle) => void | Promise<unknown>` | — | Runs on upgrade; a promise shows a pending state. |
| trigger | `ReactElement` | — | Element that opens the modal; it receives onClick, aria-haspopup and aria-expanded. |
| open / defaultOpen / onOpenChange | `boolean / boolean / (open: boolean) => void` | — | Controlled or uncontrolled open state. |

### Invite teammates

Email chip field (Enter, comma, space or paste a list; Backspace removes) that flags bad addresses, a role select and a copy-link action.

Install: `npx shadcn@latest add @geturui-pro/modal-recipes-invite-teammates`

```tsx
import { InviteTeammatesModal } from "@/components/blocks/modal-recipes/invite-teammates";
import { Button } from "@/components/ui/button";

export function Invite() {
  return <InviteTeammatesModal workspace="Acme" trigger={<Button>Invite</Button>} onInvite={async ({ emails, role }) => {}} />;
}
```

| Prop | Type | Default | Description |
|---|---|---|---|
| workspace | `string` | "Acme" | Shown in the title. |
| inviteLink | `string` | — | Copied by Copy invite link. |
| max | `number` | 20 | Max addresses per batch. |
| onInvite | `({ emails, role }) => void | Promise<unknown>` | — | Runs with valid addresses; a promise shows a pending state. |
| trigger | `ReactElement` | — | Element that opens the modal; it receives onClick, aria-haspopup and aria-expanded. |
| open / defaultOpen / onOpenChange | `boolean / boolean / (open: boolean) => void` | — | Controlled or uncontrolled open state. |

## Accessibility

- Built on Dialog: focus trap, focus restore, Escape and scrim dismissal, labelled title and description, bottom sheet below 640px.
- Forms use real labels, native radios and selects, aria-invalid with worded errors, and submit buttons linked to their form.
- Pending actions show a busy button and block dismissal; live regions announce results, copy states and filter counts.

## Source

Pro item — send `Authorization: Bearer <registry token>` to include the source.
