Team section
A people grid with an avatar, a name and a one line role for each person.
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
| Prop | Type | Default | Description |
|---|---|---|---|
| eyebrow | string | Small uppercase line above the title. | |
| title | ReactNode | The section heading. | |
| description | ReactNode | One or two sentences under the title. | |
| people | { name: string; role: string; image?: string }[] | The people, in the order shown. | |
| columns | 2 | 3 | 4 | 3 | Columns from the sm breakpoint up. |
| size | "sm" | "default" | "default" | Spacing scale. sm fits a card. |
Source
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