# V toast 12

> Team invite with per-row undo action

A shadcn/ui block from the [@cnippet](https://ui.cnippet.dev/) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/cnippet/v-toast-12).

- Page, with a live preview: https://shadcn.exchange/r/cnippet/v-toast-12
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/cnippet/v-toast-12-light-63f4058355f9d5cc.png
- Registry item: https://ui.cnippet.dev/r/v-toast-12.json
- Categories: toast
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.cnippet.dev/r/v-toast-12.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@cnippet/toast`

## Files

### `registry/default/variants/v-toast-12.tsx`

```tsx
"use client";

import { useState } from "react";
import { Button } from "@/registry/default/ui/button";
import { toastManager } from "@/registry/default/ui/toast";

const members = [
  {
    email: "olivia@example.com",
    id: "olivia",
    initials: "OM",
    name: "Olivia Martin",
    role: "Editor",
  },
  {
    email: "jackson@example.com",
    id: "jackson",
    initials: "JL",
    name: "Jackson Lee",
    role: "Viewer",
  },
  {
    email: "isabella@example.com",
    id: "isabella",
    initials: "IN",
    name: "Isabella Nguyen",
    role: "Editor",
  },
];

export function Pattern() {
  const [invited, setInvited] = useState<Set<string>>(new Set());

  const invite = (member: (typeof members)[0]) => {
    setInvited((prev) => new Set(prev).add(member.id));
    const id = toastManager.add({
      actionProps: {
        children: "Undo",
        onClick: () => {
          toastManager.close(id);
          setInvited((prev) => {
            const next = new Set(prev);
            next.delete(member.id);
            return next;
          });
          toastManager.add({
            description: `${member.name}'s invite has been revoked.`,
            title: "Invite revoked",
            type: "info",
          });
        },
      },
      description: `${member.email} will receive an email invite.`,
      timeout: 6000,
      title: `Invite sent to ${member.name}`,
      type: "success",
    });
  };

  return (
    <div className="w-full max-w-xs rounded-xl border border-border p-4">
      <p className="mb-3 font-semibold text-sm">Invite team members</p>
      <div className="flex flex-col gap-2">
        {members.map((m) => (
          <div className="flex items-center justify-between gap-2" key={m.id}>
            <div className="flex items-center gap-2.5">
              <div className="flex size-7 items-center justify-center rounded-full bg-muted font-medium text-xs">
                {m.initials}
              </div>
              <div className="flex flex-col">
                <span className="font-medium text-sm leading-none">
                  {m.name}
                </span>
                <span className="text-muted-foreground text-xs">{m.role}</span>
              </div>
            </div>
            <Button
              disabled={invited.has(m.id)}
              onClick={() => invite(m)}
              size="sm"
              variant="outline"
            >
              {invited.has(m.id) ? "Invited" : "Invite"}
            </Button>
          </div>
        ))}
      </div>
    </div>
  );
}
```
