All Taste components
Settings
Team members
Invite people by email and manage who is on the team and what each person can do.
invite row · member table · role select · pending invite · owner lock · row menu
Team
5 of 8 seats used
| Member | Role | Last active | |
|---|---|---|---|
AR Asha Ramanasha@saffronstudio.in | Owner | Now | |
AM Arjun Mehtaarjun@saffronstudio.in | 2 h ago | ||
RS Ritika Shahritika@saffronstudio.in | Yesterday | ||
KR Kabir Raokabir@saffronstudio.in | 8 Oct | ||
neha@saffronstudio.inInvited 2 days ago | Pending |
"use client";
import { useState } from "react";
import { Lock, Mail, MoreHorizontal } from "lucide-react";
type Role = "owner" | "admin" | "member" | "viewer";
export interface Member { email: string; name?: string; role: Role; lastActive?: string; pending?: boolean }
const sample: Member[] = [
{ name: "Asha Raman", email: "asha@saffronstudio.in", role: "owner", lastActive: "Now" },
{ name: "Arjun Mehta", email: "arjun@saffronstudio.in", role: "admin", lastActive: "2 h ago" },
{ name: "Ritika Shah", email: "ritika@saffronstudio.in", role: "member", lastActive: "Yesterday" },
{ name: "Kabir Rao", email: "kabir@saffronstudio.in", role: "viewer", lastActive: "8 Oct" },
{ email: "neha@saffronstudio.in", role: "member", pending: true },
];
const roles: Exclude<Role, "owner">[] = ["admin", "member", "viewer"];
const label = (r: string) => r[0].toUpperCase() + r.slice(1);
const initials = (n: string) => n.split(" ").map((w) => w[0]).join("").slice(0, 2);
export function TeamMembers({ members = sample, seats = 8, onInvite, onRoleChange, onMenu }: {
members?: Member[];
seats?: number;
onInvite?: (email: string, role: Role) => void;
onRoleChange?: (email: string, role: Role) => void;
onMenu?: (email: string) => void;
}) {
const [email, setEmail] = useState("");
const [role, setRole] = useState<Role>("member");
const field = "h-9 rounded border border-border-strong bg-bg px-3 text-sm outline-none focus:border-primary focus:ring-[3px] focus:ring-primary/20";
return (
<section className="w-full overflow-hidden rounded-lg border border-border bg-bg">
<div className="px-4 pt-4">
<h3 className="text-sm font-medium">Team</h3>
<p className="text-[12.5px] text-fg-subtle">{members.length} of {seats} seats used</p>
</div>
<form className="flex flex-wrap gap-2 p-4" onSubmit={(e) => { e.preventDefault(); if (email) { onInvite?.(email, role); setEmail(""); } }}>
<input type="email" required value={email} onChange={(e) => setEmail(e.target.value)} placeholder="name@company.com"
aria-label="Email to invite" className={field + " min-w-0 flex-[1_1_200px] placeholder:text-fg-subtle"} />
<select value={role} onChange={(e) => setRole(e.target.value as Role)} aria-label="Role for invite" className={field}>
{roles.map((r) => <option key={r} value={r}>{label(r)}</option>)}
</select>
<button type="submit" className="inline-flex h-9 items-center gap-1.5 rounded bg-primary px-3.5 text-sm font-medium text-primary-fg hover:bg-primary-hover">
<Mail className="size-4" aria-hidden /> Send invite
</button>
</form>
<div className="overflow-x-auto border-t border-border">
<table className="w-full text-[13.5px]">
<thead>
<tr className="border-b border-border text-left text-xs text-fg-subtle">
<th className="px-4 py-2 font-medium">Member</th>
<th className="px-4 py-2 font-medium">Role</th>
<th className="px-4 py-2 font-medium">Last active</th>
<th className="px-4 py-2"><span className="sr-only">Actions</span></th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{members.map((m) => (
<tr key={m.email} className="hover:bg-surface">
<td className="px-4 py-2.5">
<div className="flex items-center gap-2.5">
<span className={"grid size-7 shrink-0 place-items-center rounded-full text-[11px] font-medium " + (m.name ? "bg-primary-soft text-primary-soft-fg" : "bg-surface-2 text-fg-subtle")}>
{m.name ? initials(m.name) : <Mail className="size-3.5" aria-hidden />}
</span>
<div className="min-w-0">
<p className="whitespace-nowrap font-medium">{m.name ?? m.email}</p>
<p className="whitespace-nowrap text-[12.5px] text-fg-subtle">{m.name ? m.email : "Invite sent"}</p>
</div>
</div>
</td>
<td className="px-4 py-2.5">
{m.role === "owner" ? (
<span className="inline-flex items-center gap-1 rounded-full bg-surface-2 px-2 py-0.5 text-xs font-medium text-fg-muted">
<Lock className="size-3" aria-hidden /> Owner
</span>
) : (
<select defaultValue={m.role} aria-label={"Role for " + (m.name ?? m.email)}
onChange={(e) => onRoleChange?.(m.email, e.target.value as Role)}
className="h-[30px] rounded border border-border-strong bg-bg pl-2.5 pr-7 text-[13px] outline-none focus:border-primary">
{roles.map((r) => <option key={r} value={r}>{label(r)}</option>)}
</select>
)}
</td>
<td className="whitespace-nowrap px-4 py-2.5 text-fg-muted">
{m.pending ? <span className="rounded-full bg-warning-soft px-2 py-0.5 text-xs font-medium text-warning-fg">Pending</span> : m.lastActive}
</td>
<td className="w-11 px-4 py-2.5 text-right">
{m.role !== "owner" && (
<button onClick={() => onMenu?.(m.email)} aria-label={"More for " + (m.name ?? m.email)}
className="grid size-7 place-items-center rounded text-fg-muted hover:bg-surface-2">
<MoreHorizontal className="size-4" />
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.