Componentfeedback1.75 KB gzipZero dependencies
Invite Modal
Invite modal with email, role radio group and sent confirmation.
Preview
Live Preview
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add invite-modalOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { InviteModal } from "@/components/ui/invite-modal";
<InviteModal />Usage guidance
Use Invite Modal when a ticket stub surface needs Invite modal with email, role radio group and sent confirmation. Keep props minimal and prefer composition with sibling stubs.
Avoid Invite Modal for long form flows or dense data grids where a dedicated layout block fits better.
Source code
invite-modal.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface InviteModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
roles?: string[];
onSend?: (email: string, role: string) => void;
}
/** Team member invite modal: email + role selector + dispatch state. */
export function InviteModal({ open, onOpenChange, roles = ["admin", "editor", "member"], onSend }: InviteModalProps) {
const [email, setEmail] = React.useState("");
const [role, setRole] = React.useState(roles[roles.length - 1]);
const [sent, setSent] = React.useState(false);
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
React.useEffect(() => {
if (!open) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") onOpenChange(false);
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [open, onOpenChange]);
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={() => onOpenChange(false)}>
<style>{`
@keyframes invScale { from { opacity: 0; transform: scale(0.95) translateY(8px); } to { opacity: 1; transform: scale(1) translateY(0); } }
`}</style>
<div
role="dialog"
aria-modal="true"
aria-label="Invite team member"
onClick={(e) => e.stopPropagation()}
className="w-full max-w-sm rounded-lg border border-border bg-card p-5 shadow-lg animate-[invScale_0.25s_cubic-bezier(0.16,1,0.3,1)_both] motion-reduce:animate-none"
>
{!sent ? (
<>
<h3 className="text-sm font-semibold">Invite team member</h3>
<p className="mt-1 text-xs text-muted-foreground">An invitation email will be dispatched immediately.</p>
<div className="mt-4 space-y-3">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="ada@example.com"
aria-label="Invitation email"
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
<div className="flex gap-1.5" role="radiogroup" aria-label="Role">
{roles.map((r) => (
<button
key={r}
role="radio"
aria-checked={role === r}
onClick={() => setRole(r)}
className={cn(
"flex-1 rounded-md border px-2 py-1.5 font-mono text-[10px] uppercase tracking-wider transition-all duration-150 motion-reduce:transition-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
role === r ? "border-accent bg-accent/10 text-accent" : "border-border text-muted-foreground hover:border-foreground/40"
)}
>
{r}
</button>
))}
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<button
type="button"
onClick={() => onOpenChange(false)}
className="rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none"
>
Cancel
</button>
<button
type="button"
disabled={!valid}
onClick={() => { onSend?.(email, role); setSent(true); setTimeout(() => { setSent(false); setEmail(""); onOpenChange(false); }, 1500); }}
className="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none"
>
Send Invite
</button>
</div>
</>
) : (
<div className="py-8 text-center">
<span className="inline-flex size-10 items-center justify-center rounded-full bg-success/15 text-success animate-[invScale_0.3s_cubic-bezier(0.34,1.56,0.64,1)_both] motion-reduce:animate-none">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5" /></svg>
</span>
<p className="mt-3 text-sm font-medium">Invitation sent</p>
<p className="text-xs text-muted-foreground">{email}</p>
</div>
)}
</div>
</div>
);
}
Props
| Prop | Type | Description |
|---|---|---|
| open | boolean | |
| onOpenChange | (open: boolean) => void | |
| roles | string[] | |
| onSend | (email: string, role: string) => void |