# Team Members

> Team member list with email invites, role selects and removal; the owner is protected.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/team-members
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/team-members.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/team-members-demo.tsx"
"use client"

import { TeamMembers, type Member } from "@/components/team-members"

const members: Member[] = [
  { id: "1", name: "Maya Chen", email: "maya@acme.co", role: "owner" },
  { id: "2", name: "Jonas Weber", email: "jonas@acme.co", role: "admin" },
  { id: "3", name: "Priya Nair", email: "priya@acme.co", role: "member" },
  { id: "4", name: "Lucas Silva", email: "lucas@acme.co", role: "viewer" },
]

export default function TeamMembersDemo() {
  return <TeamMembers members={members} />
}
```

## Source

```tsx title="components/team-members.tsx"
"use client"

import * as React from "react"
import { Mail, UserMinus } from "lucide-react"

import { cn } from "@/lib/utils"

export type Role = "owner" | "admin" | "member" | "viewer"

export interface Member {
  id: string
  name: string
  email: string
  role: Role
}

export interface TeamMembersProps extends Omit<React.ComponentProps<"div">, "onChange"> {
  members: Member[]
  onChange?: (members: Member[]) => void
}

const roles: Role[] = ["admin", "member", "viewer"]
const hues = [12, 38, 150, 200, 262, 320]
const hueOf = (name: string) => hues[[...name].reduce((sum, ch) => sum + ch.charCodeAt(0), 0) % hues.length]

export function TeamMembers({ members: initial, onChange, className, ...props }: TeamMembersProps) {
  const [members, setMembers] = React.useState(initial)
  const [email, setEmail] = React.useState("")
  const [role, setRole] = React.useState<Role>("member")
  const nextId = React.useRef(0)
  const valid = /^\S+@\S+\.\S+$/.test(email.trim())

  const commit = (next: Member[]) => {
    setMembers(next)
    onChange?.(next)
  }

  return (
    <div {...props} className={cn("w-full max-w-xl rounded-2xl border bg-card", className)}>
      <form
        className="flex flex-wrap gap-2 border-b p-4"
        onSubmit={(event) => {
          event.preventDefault()
          if (!valid) return
          const address = email.trim()
          commit([...members, { id: `invite-${nextId.current++}`, name: address.split("@")[0], email: address, role }])
          setEmail("")
        }}
      >
        <div className="relative min-w-48 flex-1">
          <Mail aria-hidden className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
          <input
            type="email"
            value={email}
            onChange={(event) => setEmail(event.target.value)}
            placeholder="name@company.com"
            aria-label="Invite by email"
            className="h-10 w-full rounded-lg border border-input bg-background pl-9 pr-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/30"
          />
        </div>
        <select
          value={role}
          onChange={(event) => setRole(event.target.value as Role)}
          aria-label="Role for new member"
          className="h-10 rounded-lg border border-input bg-background px-2 text-sm capitalize outline-none focus-visible:ring-4 focus-visible:ring-ring/30"
        >
          {roles.map((r) => (
            <option key={r} value={r}>
              {r}
            </option>
          ))}
        </select>
        <button
          type="submit"
          disabled={!valid}
          className="h-10 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-opacity disabled:opacity-40"
        >
          Invite
        </button>
      </form>

      <ul className="divide-y">
        {members.map((member) => (
          <li key={member.id} className="flex items-center gap-3 px-4 py-3">
            <span
              aria-hidden
              className="flex size-9 shrink-0 items-center justify-center rounded-full text-sm font-semibold text-white"
              style={{ backgroundColor: `hsl(${hueOf(member.name)} 55% 45%)` }}
            >
              {member.name.slice(0, 1).toUpperCase()}
            </span>
            <div className="min-w-0 flex-1 leading-tight">
              <p className="truncate text-sm font-medium">{member.name}</p>
              <p className="truncate text-xs text-muted-foreground">{member.email}</p>
            </div>
            {member.role === "owner" ? (
              <span className="rounded-full bg-secondary px-2.5 py-1 text-xs font-medium text-secondary-foreground">Owner</span>
            ) : (
              <>
                <select
                  value={member.role}
                  onChange={(event) => commit(members.map((m) => (m.id === member.id ? { ...m, role: event.target.value as Role } : m)))}
                  aria-label={`Role for ${member.name}`}
                  className="h-8 rounded-md border border-input bg-background px-2 text-sm capitalize outline-none focus-visible:ring-4 focus-visible:ring-ring/30"
                >
                  {roles.map((r) => (
                    <option key={r} value={r}>
                      {r}
                    </option>
                  ))}
                </select>
                <button
                  type="button"
                  aria-label={`Remove ${member.name}`}
                  onClick={() => commit(members.filter((m) => m.id !== member.id))}
                  className="flex size-8 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-destructive/10 hover:text-destructive focus-visible:ring-2 focus-visible:ring-ring"
                >
                  <UserMinus className="size-4" />
                </button>
              </>
            )}
          </li>
        ))}
        {members.length === 0 && <li className="px-4 py-8 text-center text-sm text-muted-foreground">No members yet.</li>}
      </ul>
    </div>
  )
}
```
