Skip to content
goodtabs UI
BrowseBrowse components

Team section

A people grid with an avatar, a name and a one line role for each person.

npx shadcn@latest add https://ui.goodtabs.co/r/team-section.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/team-section

Team

Four people, one bar

  • AO

    Ada Okafor

    Design engineer

  • ML

    Mateo Lindqvist

    Frontend lead

  • NH

    Noor Haddad

    Product designer

  • PR

    Priya Raman

    Accessibility and QA

Four people in a narrow box.

Three columns

Team

The people behind the components

A small group that reviews every pull request together.

  • AO

    Ada Okafor

    Design engineer

  • ML

    Mateo Lindqvist

    Frontend lead

  • NH

    Noor Haddad

    Product designer

  • PR

    Priya Raman

    Accessibility and QA

  • JW

    Jonas Weber

    Motion and prototyping

  • LF

    Lucia Ferreira

    Developer relations

Six people with a description.

Four columns

Core team

  • AO

    Ada Okafor

    Design engineer

  • ML

    Mateo Lindqvist

    Frontend lead

  • NH

    Noor Haddad

    Product designer

  • PR

    Priya Raman

    Accessibility and QA

A compact core team row.

Usage

import { TeamSection } from "@/components/team-section"

<TeamSection title="Core team" people={[{ name: "Ada Okafor", role: "Design engineer" }]} />

Props

PropTypeDefaultDescription
eyebrowstringSmall uppercase line above the title.
titleReactNodeThe section heading.
descriptionReactNodeOne or two sentences under the title.
people{ name: string; role: string; image?: string }[]The people, in the order shown.
columns2 | 3 | 43Columns from the sm breakpoint up.
size"sm" | "default""default"Spacing scale. sm fits a card.

Source

team-section/team-section.tsx
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { cn } from "@/lib/utils";

export type TeamPerson = {
  /** Full name. Used for the alt text and the initials fallback. */
  name: string;
  /** One line on what they do. */
  role: string;
  /** Avatar url. Initials show when it is missing. */
  image?: string;
};

type TeamSectionProps = Omit<React.ComponentProps<"section">, "title"> & {
  /** Small uppercase line above the title. */
  eyebrow?: string;
  /** The section heading. */
  title?: React.ReactNode;
  /** One or two sentences under the title. */
  description?: React.ReactNode;
  /** The people, in the order shown. */
  people: TeamPerson[];
  /** Columns from the sm breakpoint up. */
  columns?: 2 | 3 | 4;
  /** Spacing scale. "sm" fits a card or narrow column. */
  size?: "sm" | "default";
};

const cols = {
  2: "sm:grid-cols-2",
  3: "sm:grid-cols-2 lg:grid-cols-3",
  4: "sm:grid-cols-2 lg:grid-cols-4",
};

function initials(name: string) {
  const parts = name.trim().split(/\s+/);
  return ((parts[0]?.[0] ?? "") + (parts[1]?.[0] ?? "")).toUpperCase();
}

/**
 * A people grid: a heading, then an avatar, a name and a role line for each person.
 * Server component. Avatars fall back to initials.
 */
export function TeamSection({
  eyebrow,
  title,
  description,
  people,
  columns = 3,
  size = "default",
  className,
  ...props
}: TeamSectionProps) {
  const sm = size === "sm";
  return (
    <section className={cn(sm ? "py-8" : "py-16 lg:py-24", className)} {...props}>
      <div className="mx-auto w-full max-w-6xl px-4 md:px-6">
        {(eyebrow || title || description) && (
          <header className={cn("flex max-w-2xl flex-col gap-2", sm ? "mb-6" : "mb-10")}>
            {eyebrow && (
              <p className="m-0 font-mono text-xs tracking-wide text-muted-foreground uppercase">{eyebrow}</p>
            )}
            {title && (
              <h2 className={cn("m-0 font-medium tracking-tight text-balance", sm ? "text-xl" : "text-3xl")}>
                {title}
              </h2>
            )}
            {description && <p className="m-0 text-sm text-muted-foreground sm:text-base">{description}</p>}
          </header>
        )}
        <ul className={cn("m-0 grid list-none grid-cols-1 gap-3 p-0", cols[columns])}>
          {people.map((p) => (
            <li key={p.name} className="flex items-center gap-3 rounded-xl border bg-card p-3 text-card-foreground">
              <Avatar size="lg">
                {p.image && <AvatarImage src={p.image} alt="" />}
                <AvatarFallback>{initials(p.name)}</AvatarFallback>
              </Avatar>
              <div className="flex min-w-0 flex-col">
                <p className="m-0 truncate text-sm font-medium">{p.name}</p>
                <p className="m-0 truncate text-sm text-muted-foreground">{p.role}</p>
              </div>
            </li>
          ))}
        </ul>
      </div>
    </section>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/team-section/prompt.md, or the Copy AI prompt button above.

When to use

  • An about or company page
  • When a face, a name and a role are enough

When not to use

  • For bios and links, use a card per person
  • For a team inside an app, use team-card

Agent notes

  • Server component; avatars fall back to initials when image is missing
  • Demo names are fictional
  • Role lines truncate on one line, keep them short