Website Sections
TeamGrid
People cards that rise in on scroll. Hover or focus a card and its bio opens while the others dim. On touch screens the bios stay open.
Download
- React
- Tailwind CSS v4
- motion
components/ui/team-grid.tsx
"use client";
import { useId, type CSSProperties, type ElementType, type ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
export type TeamGridMember = {
name: string;
role: string;
/** Picture URL. Without one the card shows the initials. */
photo?: string;
/** Opens on hover or focus. Always open on touch screens. */
bio?: string;
/** Social or profile links, under the bio. */
links?: { label: string; href: string }[];
};
type TeamGridProps = {
members: TeamGridMember[];
title?: string;
/** Small line above the title. */
eyebrow?: string;
subtitle?: string;
/** Goes under the grid: a hiring note or a link. */
children?: ReactNode;
/** Smallest card width in pixels. Sets how many fit in a row. */
minWidth?: number;
/** Card colour. */
tile?: string;
/** Card edge colour on hover. */
color?: string;
/** h2 by default. */
headingAs?: ElementType;
/** Set padding and colours here. */
className?: string;
};
const initials = (name: string) =>
name
.split(/\s+/)
.slice(0, 2)
.map((word) => word[0])
.join("")
.toUpperCase();
/* TeamGrid: people cards that rise in on scroll. Hover or focus a card and its bio opens while the others dim. On touch screens the bios stay open. */
export function TeamGrid({
members,
title,
eyebrow,
subtitle,
children,
minWidth = 210,
tile = "#15122a",
color = "#a78bfa",
headingAs: Heading = "h2",
className = "bg-[#0b0a14] px-5 py-16 text-white",
}: TeamGridProps) {
const reduce = useReducedMotion();
const id = useId();
return (
<section aria-labelledby={title ? id : undefined} className={`@container ${className}`}>
{title && (
<header className="mx-auto mb-8 max-w-2xl text-center">
{eyebrow && <p className="mb-2 text-xs font-medium tracking-[0.2em] uppercase opacity-70">{eyebrow}</p>}
<Heading id={id} className="text-[clamp(1.5rem,6cqw,2.5rem)] leading-tight font-semibold tracking-tight text-balance">
{title}
</Heading>
{subtitle && <p className="mt-3 text-pretty opacity-70">{subtitle}</p>}
</header>
)}
<ul
className="mx-auto grid max-w-6xl items-start gap-3 @3xl:gap-4"
style={{ gridTemplateColumns: `repeat(auto-fit, minmax(min(${minWidth}px, 100%), 1fr))` }}
>
{members.map((member, i) => (
<motion.li
key={member.name}
initial={reduce ? { opacity: 0 } : { opacity: 0, y: 20 }}
whileInView={reduce ? { opacity: 1 } : { opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.15 }}
transition={{ duration: 0.6, delay: Math.min(i, 6) * 0.06, ease: [0.16, 1, 0.3, 1] }}
>
<article
tabIndex={member.bio && !member.links?.length ? 0 : undefined}
className="group/card overflow-hidden rounded-2xl border border-current/10 outline-offset-2 transition-[border-color,box-shadow,opacity] duration-300 hover:border-[var(--tg-edge)] hover:shadow-[0_18px_40px_-24px_var(--tg-edge)] focus-within:border-[var(--tg-edge)] [ul:has(article:hover)_&:not(:hover)]:opacity-55"
style={{ backgroundColor: tile, "--tg-edge": color } as CSSProperties}
>
<div className="relative grid aspect-[5/4] place-items-center overflow-hidden" style={{ background: `linear-gradient(150deg, ${color}40, transparent 70%)` }}>
{member.photo ? (
<img
src={member.photo}
alt=""
className="absolute inset-0 size-full object-cover transition-transform duration-500 ease-out group-hover/card:scale-105 motion-reduce:transition-none motion-reduce:group-hover/card:scale-100"
/>
) : (
<span aria-hidden className="text-4xl font-semibold tracking-tight opacity-80">
{initials(member.name)}
</span>
)}
</div>
<div className="p-4">
<h3 className="font-semibold">{member.name}</h3>
<p className="text-sm opacity-70">{member.role}</p>
{(member.bio || member.links?.length) && (
<div className="grid grid-rows-[0fr] transition-[grid-template-rows] duration-300 ease-out group-focus-within/card:grid-rows-[1fr] group-hover/card:grid-rows-[1fr] pointer-coarse:grid-rows-[1fr] motion-reduce:transition-none">
<div className="min-h-0 overflow-hidden">
{member.bio && <p className="pt-3 text-sm text-pretty opacity-80">{member.bio}</p>}
{member.links && member.links.length > 0 && (
<ul className="flex flex-wrap gap-x-3 gap-y-1 pt-3 text-sm">
{member.links.map((link) => (
<li key={link.label}>
<a href={link.href} className="underline underline-offset-2" style={{ color }} aria-label={`${member.name}: ${link.label}`}>
{link.label}
</a>
</li>
))}
</ul>
)}
</div>
</div>
)}
</div>
</article>
</motion.li>
))}
</ul>
{children && <div className="mx-auto mt-8 max-w-6xl text-center">{children}</div>}
</section>
);
}
1. Install the dependency.
npm i motion2. Paste the Code tab into
components/ui/team-grid.tsx. The styles are Tailwind CSS v4 classes.3. Use it. This is the preview above:
app/team-grid-demo.tsx
import { TeamGrid } from "@/components/ui/team-grid"; const MEMBERS = [ { name: "Mira Sol", role: "Founder", photo: "/demo/dawn.jpg", bio: "Sketches every new idea on paper first.", links: [ { label: "Profile", href: "#mira" }, { label: "Writing", href: "#mira-writing" }, ], }, { name: "Theo Lane", role: "Design lead", photo: "/demo/dusk.jpg", bio: "Cares about the last two pixels." }, { name: "Asha Rao", role: "Engineering", bio: "Keeps the build fast and the pager quiet." }, { name: "Jonas Vale", role: "Support", photo: "/demo/moss.jpg", bio: "Answers within the hour, every time.", links: [{ label: "Profile", href: "#jonas" }] }, ]; export default function TeamGridExample() { return ( <div className="h-full overflow-y-auto"> <TeamGrid title="The people behind it" members={MEMBERS} headingAs="h3" minWidth={170} className="min-h-full bg-[#0b0a14] p-4 text-white" /> </div> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| members | TeamGridMember[] | — | Each has name and role, plus optional photo, bio and links. |
| title | string | — | Heading above the cards. |
| eyebrow | string | — | Small line above the title. |
| subtitle | string | — | Line under the title. |
| children | ReactNode | — | Goes under the grid: a hiring note or a link. |
| minWidth | number | 210 | Smallest card width in pixels. |
| tile | string | "#15122a" | Card colour. |
| color | string | "#a78bfa" | Card edge colour on hover. |
| headingAs | ElementType | "h2" | Heading tag. |
| className | string | "bg-[#0b0a14] px-5 py-16 text-white" | Set padding and colours here. |