# Team Block

> A Team Block component for displaying a team section.

A shadcn/ui block from the [@devsloka](https://ui.devsloka.in/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/devsloka/team-block).

- Page, with a live preview: https://shadcn.exchange/r/devsloka/team-block
- Screenshot: https://shadcn.exchange/shot/devsloka/team-block/light.png
- Registry item: https://ui.devsloka.in/r/team-block.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/devsloka/team-block.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`

## Files

### `src/registry/blocks/teams/team-block.tsx`

```tsx
import { useId, type ComponentProps } from "react";
import { ArrowUpRight } from "lucide-react";
import { cn } from "@/lib/utils";

export type TeamMember = {
  name: string;
  role: string;
  /** City and timezone, e.g. "Lisbon · UTC+0". */
  location: string;
  /** One short, personal line. */
  note?: string;
  /** Portrait URL (4:5 works best). Falls back to an initials monogram. */
  image?: string;
};

export type TeamHiring = {
  label: string;
  href: string;
  linkLabel?: string;
};

export type TeamBlockProps = {
  eyebrow?: string;
  title?: string;
  description?: string;
  members?: TeamMember[];
  hiring?: TeamHiring | null;
} & ComponentProps<"section">;

const portrait = (id: string) =>
  `https://images.unsplash.com/${id}?w=600&h=750&fit=crop&crop=faces`;

const DEFAULT_MEMBERS: TeamMember[] = [
  {
    name: "Ines Carvalho",
    role: "Founder, Design",
    location: "Lisbon · UTC+0",
    note: "Still sketches every screen on paper first.",
    image: portrait("photo-1438761681033-6461ffad8d80"),
  },
  {
    name: "Theo Lindqvist",
    role: "Engineering",
    location: "Stockholm · UTC+1",
    note: "Wrote our sync engine twice. The second one stuck.",
    image: portrait("photo-1500648767791-00dcc994a43e"),
  },
  {
    name: "Adaeze Nwosu",
    role: "Product",
    location: "Lagos · UTC+1",
    note: "Reads every support ticket, including the angry ones.",
    image: portrait("photo-1531123897727-8f129e1688ce"),
  },
  {
    name: "Kenji Mori",
    role: "Engineering",
    location: "Osaka · UTC+9",
    note: "Keeps the p95 under 80ms and the build under a minute.",
    image: portrait("photo-1506794778202-cad84cf45f1d"),
  },
  {
    name: "Clara Weiss",
    role: "Customer Success",
    location: "Berlin · UTC+1",
    note: "Has onboarded 300-odd teams and remembers most of them.",
    image: portrait("photo-1544005313-94ddf0286df2"),
  },
  {
    name: "Marcus Bell",
    role: "Brand & Writing",
    location: "Toronto · UTC−5",
    note: "Deletes adjectives for a living.",
    image: portrait("photo-1472099645785-5658abf4ff4e"),
  },
  {
    name: "Sofía Ramírez",
    role: "Design Engineering",
    location: "Mexico City · UTC−6",
    note: "Owns the component library and the coffee budget.",
    image: portrait("photo-1534528741775-53994a69daeb"),
  },
  {
    name: "Arjun Mehta",
    role: "Infrastructure",
    location: "Bengaluru · UTC+5:30",
    note: "On call this week. Probably asleep right now.",
    image: portrait("photo-1539571696357-5a69c17a67c6"),
  },
];

const DEFAULT_HIRING: TeamHiring = {
  label: "We're hiring — 3 open roles in engineering and support",
  href: "#careers",
  linkLabel: "See open roles",
};

const pad = (n: number) => String(n).padStart(2, "0");

const initials = (name: string) =>
  name
    .split(" ")
    .map((part) => part[0])
    .slice(0, 2)
    .join("");

export function TeamBlock({
  eyebrow = "Team",
  title = "Eight people, five timezones, one product",
  description = "Halden is built by a small, fully remote team. We overlap for two hours a day and write everything else down.",
  members = DEFAULT_MEMBERS,
  hiring = DEFAULT_HIRING,
  className,
  ...props
}: TeamBlockProps) {
  const titleId = useId();

  return (
    <section
      data-slot="team-block"
      aria-labelledby={titleId}
      className={cn("w-full bg-background text-foreground", className)}
      {...props}
    >
      <div className="mx-auto max-w-6xl px-6 py-20 md:py-28">
        <div className="grid grid-cols-1 gap-x-8 gap-y-6 border-t pt-8 md:grid-cols-12">
          <p className="font-mono text-xs uppercase tracking-wider text-muted-foreground md:col-span-4">
            {eyebrow}
            <span className="ml-2 tabular-nums">({pad(members.length)})</span>
          </p>
          <div className="md:col-span-8">
            <h2
              id={titleId}
              className="max-w-xl text-3xl font-medium tracking-tight text-balance md:text-5xl md:tracking-[-0.03em]"
            >
              {title}
            </h2>
            {description && (
              <p className="mt-5 max-w-md text-base leading-relaxed text-muted-foreground">
                {description}
              </p>
            )}
          </div>
        </div>

        <ul className="mt-14 grid grid-cols-2 gap-x-4 gap-y-10 sm:gap-x-6 md:grid-cols-3 lg:grid-cols-4">
          {members.map((member, i) => (
            <li key={`${member.name}-${i}`} className="group">
              <div className="relative aspect-[4/5] overflow-hidden rounded-sm bg-muted">
                {member.image ? (
                  // eslint-disable-next-line @next/next/no-img-element -- plain <img> keeps the block portable outside Next.js
                  <img
                    src={member.image}
                    alt={`Portrait of ${member.name}`}
                    width={600}
                    height={750}
                    loading="lazy"
                    className="size-full object-cover grayscale transition-[filter] duration-500 group-hover:grayscale-0 motion-reduce:transition-none"
                  />
                ) : (
                  <span
                    aria-hidden="true"
                    className="flex size-full items-center justify-center font-mono text-2xl text-muted-foreground"
                  >
                    {initials(member.name)}
                  </span>
                )}
              </div>
              <div className="mt-4 flex items-baseline justify-between gap-3 border-t pt-3">
                <h3 className="text-sm font-medium">{member.name}</h3>
                <span
                  aria-hidden="true"
                  className="font-mono text-xs tabular-nums text-muted-foreground"
                >
                  {pad(i + 1)}
                </span>
              </div>
              <p className="mt-1 text-sm text-muted-foreground">{member.role}</p>
              <p className="mt-2 font-mono text-[0.7rem] uppercase tracking-wider text-muted-foreground">
                {member.location}
              </p>
              {member.note && (
                <p className="mt-3 text-sm leading-snug text-pretty">
                  {member.note}
                </p>
              )}
            </li>
          ))}
        </ul>

        {hiring && (
          <div className="mt-16 flex flex-col gap-3 border-y py-5 sm:flex-row sm:items-center sm:justify-between">
            <p className="flex items-center gap-3 text-sm">
              <span aria-hidden="true" className="size-2 shrink-0 bg-primary" />
              {hiring.label}
            </p>
            <a
              href={hiring.href}
              className="group/link inline-flex items-center gap-1 font-mono text-xs uppercase tracking-wider underline-offset-4 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
            >
              {hiring.linkLabel ?? "Open roles"}
              <ArrowUpRight
                aria-hidden="true"
                className="size-3.5 transition-transform group-hover/link:-translate-y-0.5 group-hover/link:translate-x-0.5 motion-reduce:transition-none"
              />
            </a>
          </div>
        )}
      </div>
    </section>
  );
}
```
