Team Grid
A team section with a heading, intro, and a responsive grid of member photo cards that reveal role on hover.
import type { Block } from 'payload'import { teamFields, teamMemberFields } from '@/blocks/shared/teamFields'export const TeamGrid: Block = { slug: 'teamGrid', interfaceName: 'TeamGridBlock', fields: [ // Shared team core (eyebrow, title). Variant-specific fields follow; edit the // shared shape in @/blocks/shared/teamFields. ...teamFields, { name: 'description', type: 'textarea', }, { name: 'members', type: 'array', required: true, minRows: 1, maxRows: 12, admin: { initCollapsed: true, }, // Shared member shape — see @/blocks/shared/teamFields. fields: teamMemberFields, }, ], labels: { plural: 'Team Grid Blocks', singular: 'Team Grid', },}import React from 'react'import type { TeamGridBlock as TeamGridBlockData } from '@/payload-types'import { Media } from '@/components/Media'import { Badge } from '@/components/ui/badge'import { cn } from '@/utilities/ui'type Props = TeamGridBlockData & { id?: string className?: string disableInnerContainer?: boolean}export const TeamGridBlock: React.FC<Props> = ({ className, description, disableInnerContainer, eyebrow, id, members, title,}) => { return ( <section className={cn('container', className)} id={id ? `block-${id}` : undefined}> <div className="overflow-hidden rounded-frame border border-border/70 bg-card/35 px-6 py-10 sm:px-8 lg:px-12 lg:py-14"> <div className={cn('flex flex-col gap-12', { 'mx-auto max-w-5xl': !disableInnerContainer, })} > <div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between"> <div className="flex flex-col gap-4"> {eyebrow ? ( <Badge variant="outline" className="w-fit rounded-full px-3 py-1 uppercase tracking-eyebrow"> {eyebrow} </Badge> ) : null} <h2 className="text-3xl font-medium tracking-title text-balance sm:text-4xl">{title}</h2> </div> {description ? ( <p className="max-w-md text-base leading-7 text-muted-foreground">{description}</p> ) : null} </div> {members && members.length > 0 ? ( <div className="grid gap-x-6 gap-y-12 sm:grid-cols-2 lg:grid-cols-3"> {members.map((member, index) => ( <div className="group overflow-hidden" key={member.id ?? index}> <div className="h-96 w-full overflow-hidden rounded-md transition-all duration-500 group-hover:rounded-xl"> <Media resource={member.avatar} imgClassName="h-full w-full object-cover object-top grayscale transition-all duration-500 group-hover:grayscale-0" /> </div> <div className="px-2 pt-4"> <div className="flex items-center justify-between"> <h3 className="text-base font-medium transition-all duration-500 group-hover:tracking-wider"> {member.name} </h3> <span className="text-xs text-muted-foreground">_0{index + 1}</span> </div> <div className="mt-1 flex items-center justify-between"> <span className="translate-y-6 text-sm text-muted-foreground opacity-0 transition duration-300 group-hover:translate-y-0 group-hover:opacity-100"> {member.role} </span> {member.href ? ( <a className="inline-block translate-y-8 text-sm tracking-wide opacity-0 transition-all duration-500 hover:underline group-hover:translate-y-0 group-hover:opacity-100" href={member.href} > Profile </a> ) : null} </div> </div> </div> ))} </div> ) : null} </div> </div> </section> )}import type { Field } from 'payload'/** * Shared field core for the Team component family. * * Every team variant (team-roster, team-grid, …) spreads `teamFields` first for * the shared heading, then appends its own variant-specific shape — the grouped * department roster or the flat member grid that differs per layout. Editing the * shared eyebrow/title shape here updates every installed team block at once, so * the family never drifts field-by-field across a repo. * * `teamMemberFields` is the one-person shape (avatar upload, name, role, optional * link) reused by both variants — team-roster nests it inside each group, while * team-grid uses it for a single flat `members` array — so a member looks the * same everywhere it appears. Each avatar is an editable Media upload, so editors * manage the team from the admin instead of shipping hardcoded image URLs. * * Installed once per repo at `src/blocks/shared/teamFields.ts`; re-running * `payload-components add team-*` never overwrites a copy you have already edited. */export const teamFields: Field[] = [ { name: 'eyebrow', type: 'text', }, { name: 'title', type: 'text', required: true, },]export const teamMemberFields: Field[] = [ { name: 'avatar', type: 'upload', relationTo: 'media', required: true, }, { name: 'name', type: 'text', required: true, }, { name: 'role', type: 'text', required: true, }, { name: 'href', type: 'text', },]Installation
npx payload-components add team-gridCopy the files straight from the registry, then wire the Payload fragments by hand:
pnpm dlx shadcn@latest add https://www.payload-components.xyz/r/team-grid.jsonWhat it installs
Copies 3 source files into your project:
src/blocks/shared/teamFields.tssharedsrc/blocks/TeamGrid/config.tssrc/blocks/TeamGrid/Component.tsx
…and makes 4 edits to wire the block into your project:
| Registers the block | src/collections/Pages/index.ts |
| Maps the renderer | src/blocks/RenderBlocks.tsx |
| Regenerates types | src/payload-types.ts |
| Regenerates the admin import map | src/app/(payload)/admin/importMap.js |
teamFields.ts is the shared field core for this family — every variant composes it. Editing it updates each installed block at once, and re-running an install never overwrites a copy you have changed.Re-running the install converges: it detects existing wiring, skips it, and records install state in .payload-components/state.json.
Content model
The eyebrow and title fields come from the shared Team family base
(src/blocks/shared/teamFields.ts); the description intro and the members grid are
specific to this variant.
Prop
Type
Each item in members carries:
Prop
Type
Usage
TeamGrid block to its layout.RenderBlocks on the frontend, fully typed — no extra wiring.Requirements
- Target
- payload-website-starter
- Payload
- v3
- Next.js
- 15 / 16
- shadcn UI
- badge
Your project must already expose components.json, src/payload.config.ts, src/blocks/RenderBlocks.tsx, src/collections/Pages/index.ts — the surfaces payload-components add patches. The CLI verifies this against the support matrix before touching anything.