Skip to content
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

MemberRoleLast active
AR
Asha Ramanasha@saffronstudio.in
OwnerNow
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.