Skip to content
Motion
FreeIn reviewv1.0.0

Team Grid

Real people with their photos or monograms, names and roles, as portraits or a compact list.

npx shadcn@latest add @vibebox/team-grid

Controls

Variant
<TeamGrid />

About

A new section type for Launchpad (the site kit had none). Honest by construction: it never shows stock faces; monograms and fill-ins stand in until real people are named.

Use it when

  • Customers choose the person as much as the business (clinics, salons, firms).

Skip it when

  • The team is one person: use about-story.

Install

  1. 1Add the VibeBox registry to components.json:
    {
      "registries": {
        "@vibebox": "https://motion.vibeboxph.com/r/{name}.json"
      }
    }
  2. 2Add the block with the shadcn CLI:
    npx shadcn@latest add @vibebox/team-grid
  3. 3Use it:
    import { TeamGrid } from "@/components/vibebox/team-grid";
    import { home } from "@/content/pages/home";
    
    <TeamGrid content={home.team} variant="portraits" id="team" />

Code

2 files
components/vibebox/team-grid/index.tsx
/**
 * VibeBox Motion · Team Grid
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
import { Container, headingId, Icon, initials, Media, REVEAL, Section, SectionHead, SlotText, type BlockProps } from "@/lib/vibebox/block-kit";
import { isFillIn } from "@/lib/vibebox/content/runtime";
import { cn } from "@/lib/utils";
import type { Content, Variant } from "./schema";

/*
 * The real people behind the business: photos when given, otherwise a monogram from the name
 * (or a neutral mark while the name is still a fill-in). Names and roles are facts.
 */

type Member = Content["members"][number];

function Portrait({ member, round }: { member: Member; round?: boolean }) {
  if (member.photo) return <Media image={member.photo} sizes="(min-width: 1024px) 25vw, 50vw" />;
  const shape = cn("grid size-full place-items-center bg-[linear-gradient(150deg,color-mix(in_oklab,var(--spectrum-2)_22%,var(--card)),color-mix(in_oklab,var(--spectrum-5)_16%,var(--card)))]", round && "rounded-full");
  return (
    <div aria-hidden="true" className={shape}>
      {isFillIn(member.name) ? (
        <Icon name="users" className="size-1/4 text-muted-foreground" />
      ) : (
        <span className="font-display text-[clamp(1rem,4vw,2.5rem)] font-semibold tracking-tight text-foreground/80">{initials(member.name)}</span>
      )}
    </div>
  );
}

export type TeamGridProps = BlockProps<Content, Variant>;

export function TeamGrid({ content, variant = "portraits", id, tone, className }: TeamGridProps) {
  const compact = variant === "compact";
  return (
    <Section slug="team-grid" variant={variant} id={id} tone={tone} labelledBy={headingId(id)} className={className}>
      <Container className="grid gap-[clamp(32px,4vw,56px)]">
        <SectionHead eyebrow={content.eyebrow} heading={content.heading} lead={content.lead} id={id} />
        <ul className={cn("grid", compact ? "gap-x-8 gap-y-6 sm:grid-cols-2 lg:grid-cols-3" : "grid-cols-2 gap-x-4 gap-y-8 sm:gap-x-6 lg:grid-cols-4")}>
          {content.members.map((member, index) => (
            <li key={index} className={cn(compact ? "flex min-w-0 items-center gap-4" : "grid content-start gap-3", REVEAL)}>
              <div className={cn("shrink-0 overflow-hidden ring-1 ring-border", compact ? "size-16 rounded-full" : "aspect-[4/5] rounded-xl")}>
                <Portrait member={member} round={compact} />
              </div>
              <div className="grid min-w-0 gap-0.5">
                <SlotText as="h3" value={member.name} path={`members.${index}.name`} className="font-semibold tracking-tight" />
                <SlotText as="p" value={member.role} path={`members.${index}.role`} className="text-small text-muted-foreground" />
              </div>
            </li>
          ))}
        </ul>
      </Container>
    </Section>
  );
}
components/vibebox/team-grid/schema.ts
/**
 * VibeBox Motion · Team Grid
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
import { sections, type z } from "@/lib/vibebox/content";

export const variants = ["portraits", "compact"] as const;

export type Variant = (typeof variants)[number];

export const content = sections.team;

export type Content = z.infer<typeof content>;

Content

Typed slots from schema.ts. Facts (hours, prices, quotes, names) come only from the business; until then they render as fill-ins.

SlotLabelTypeLimitFact
eyebrowKickertext32 chars—
headingHeadingtext60 chars—
leadIntrotext160 chars—
membersPeoplelistup to 12—
members[].nameNamefact48 charsYes, a fill-in until given
members[].roleRolefact48 charsYes, a fill-in until given
members[].photoPhotoimage——

Variants

  • Portraitsdefault

    portraits

    Two columns on phones, four on desktop, 4:5 portraits.

  • Compact

    compact

    Round avatars beside names, three columns on desktop.

Accessibility and motion

Keyboard
Nothing to focus.
Screen readers
A heading and a list of people, each name a heading with the role after it; monograms are hidden.
Touch
Nothing to touch.
Reduced motion
Portraits appear without their reveal.

Edge cases and limits

  • Twelve people make three portrait rows.
  • A name still a fill-in shows a neutral mark, never initials of the hint.
  • Four team portraits in a spectrum duotone with names and roles

    Portraits in a two-tone wash of the brand's spectrum that bloom into full colour on hover or focus.

    Team block · Server

  • A tall horizon scene beside the story of a family dental clinic and three details

    The business in its own words: a tall photo with a caption beside the story and a few honest details.

    About block · Server

  • A headline about moving better beside a mosaic of a tile scene, a bloom scene and a class list

    The promise beside a three-tile mosaic: a tall photo, a second image and a card of what the business offers.

    Heroes block · Server

↑ ↓ to move↵ to openesc to close