04Team members grid
멤버 카드를 클릭하거나 Enter 를 누르면 aria-expanded 가 바뀌며 한줄 소개가 grid-template-rows 로 실제로 펼쳐집니다.
Best for
aria-expandedgrid-template-rowsclick

<main class="stage">
<section class="tm">
<p class="tm__title">
<span class="tm__ic" aria-hidden="true"><svg viewBox="0 0 256 256" width="18" height="18"><g fill="currentColor"><path d="M168 144a40 40 0 1 1-40-40a40 40 0 0 1 40 40M64 56a32 32 0 1 0 32 32a32 32 0 0 0-32-32m128 0a32 32 0 1 0 32 32a32 32 0 0 0-32-32" opacity=".2"/><path d="M244.8 150.4a8 8 0 0 1-11.2-1.6A51.6 51.6 0 0 0 192 128a8 8 0 0 1 0-16a24 24 0 1 0-23.24-30a8 8 0 1 1-15.5-4A40 40 0 1 1 219 117.51a67.94 67.94 0 0 1 27.43 21.68a8 8 0 0 1-1.63 11.21M190.92 212a8 8 0 1 1-13.85 8a57 57 0 0 0-98.15 0a8 8 0 1 1-13.84-8a72.06 72.06 0 0 1 33.74-29.92a48 48 0 1 1 58.36 0A72.06 72.06 0 0 1 190.92 212M128 176a32 32 0 1 0-32-32a32 32 0 0 0 32 32m-56-56a8 8 0 0 0-8-8a24 24 0 1 1 23.24-30a8 8 0 1 0 15.5-4A40 40 0 1 0 37 117.51a67.94 67.94 0 0 0-27.4 21.68a8 8 0 1 0 12.8 9.61A51.6 51.6 0 0 1 64 128a8 8 0 0 0 8-8"/></g></svg></span>
Meet the team
</p>
<ul class="tm__grid">
<li class="tm__card">
<button type="button" class="tm__head" aria-expanded="false">
<span class="tm__avatar">L</span>
<span class="tm__meta"><b>Yuna Lee</b><i>Product design</i></span>
</button>
<div class="tm__bio"><p>Polishing a component library for 3 years. Prefers grids to coffee.</p></div>
</li>
<li class="tm__card">
<button type="button" class="tm__head" aria-expanded="false">
<span class="tm__avatar">P</span>
<span class="tm__meta"><b>Dohyun Park</b><i>Frontend</i></span>
</button>
<div class="tm__bio"><p>Never ships an interaction without accessibility.</p></div>
</li>
<li class="tm__card">
<button type="button" class="tm__head" aria-expanded="false">
<span class="tm__avatar">C</span>
<span class="tm__meta"><b>Haeun Choi</b><i>Customer success</i></span>
</button>
<div class="tm__bio"><p>Believes the first week matters most. Always happy to answer questions.</p></div>
</li>
</ul>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-enter;
$easing: $ease-spring;
$color: $subject-pink;
@include stage($stage-paper);
.tm { width: min(300px, 94vw); }
.tm__title { display: flex; align-items: center; gap: $sp-1; margin: 0 0 $sp-3; font-weight: 800; font-size: 13px; line-height: 1.2; font-family: inherit; color: $subject-ink; }
.tm__ic { display: grid; place-items: center; color: $color; }
.tm__grid { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: $sp-2; }
.tm__card { border: 1px solid rgba(23, 20, 26, .1); border-radius: $r-card; background: $stage-paper; box-shadow: $shadow-press; overflow: hidden; }
.tm__head {
display: flex; align-items: center; gap: $sp-3; width: 100%;
padding: $sp-2 $sp-3;
border: 0; background: transparent; cursor: pointer; text-align: left;
}
.tm__avatar {
display: grid; place-items: center; flex: 0 0 auto;
width: 32px; height: 32px; border-radius: 50%;
background: $color; color: $subject-ink; // 핑크 면 위 흰 글자 1.93:1 — 잉크 9.43:1
font-weight: 800; font-size: 13px; line-height: 1;
transition: transform $duration $easing;
}
.tm__card.is-open .tm__avatar { transform: scale(1.12) rotate(-6deg); }
.tm__meta { display: flex; flex-direction: column; font-family: inherit; }
.tm__meta b { font-size: 12px; color: $subject-ink; }
.tm__meta i { font-style: normal; font-size: 11px; color: rgba(23, 20, 26, .72); }
.tm__bio {
display: grid; grid-template-rows: 0fr; transition: grid-template-rows $duration $easing;
}
.tm__card.is-open .tm__bio { grid-template-rows: 1fr; }
.tm__bio p { margin: 0; overflow: hidden; padding: 0 $sp-3; font-size: 11px; line-height: 1.5; color: rgba(23, 20, 26, .68); font-family: inherit; }
.tm__card.is-open .tm__bio p { padding: 0 $sp-3 $sp-3; }
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.tm__avatar, .tm__bio { transition: none !important; }
}
// ── 반응형(320×200) ──────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.tm { width: min(260px, 92vw); }
.tm__title { margin: 0 0 4px; font-size: 11px; }
.tm__grid { gap: 4px; }
.tm__head { padding: $sp-1 $sp-2; gap: $sp-2; }
.tm__avatar { width: 20px; height: 20px; font-size: 9px; }
.tm__meta b { font-size: 10px; }
.tm__meta i { font-size: 9px; }
.tm__bio p {
font-size: 9px; line-height: 1.3;
display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
}
.tm__card.is-open .tm__bio p { padding: 0 $sp-2 4px; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.tm {
width: min(300px, 94vw);
}
.tm__title {
display: flex;
align-items: center;
gap: 4px;
margin: 0 0 12px;
font-weight: 800;
font-size: 13px;
line-height: 1.2;
font-family: inherit;
color: #17141a;
}
.tm__ic {
display: grid;
place-items: center;
color: #ff9ebb;
}
.tm__grid {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.tm__card {
border: 1px solid rgba(23, 20, 26, 0.1);
border-radius: 16px;
background: #ffffff;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.tm__head {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 8px 12px;
border: 0;
background: transparent;
cursor: pointer;
text-align: left;
}
.tm__avatar {
display: grid;
place-items: center;
flex: 0 0 auto;
width: 32px;
height: 32px;
border-radius: 50%;
background: #ff9ebb;
color: #17141a;
font-weight: 800;
font-size: 13px;
line-height: 1;
transition: transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.tm__card.is-open .tm__avatar {
transform: scale(1.12) rotate(-6deg);
}
.tm__meta {
display: flex;
flex-direction: column;
font-family: inherit;
}
.tm__meta b {
font-size: 12px;
color: #17141a;
}
.tm__meta i {
font-style: normal;
font-size: 11px;
color: rgba(23, 20, 26, 0.72);
}
.tm__bio {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.tm__card.is-open .tm__bio {
grid-template-rows: 1fr;
}
.tm__bio p {
margin: 0;
overflow: hidden;
padding: 0 12px;
font-size: 11px;
line-height: 1.5;
color: rgba(23, 20, 26, 0.68);
font-family: inherit;
}
.tm__card.is-open .tm__bio p {
padding: 0 12px 12px;
}
@media (prefers-reduced-motion: reduce) {
.tm__avatar, .tm__bio {
transition: none !important;
}
}
@media (max-width: 340px), (max-height: 220px) {
.tm {
width: min(260px, 92vw);
}
.tm__title {
margin: 0 0 4px;
font-size: 11px;
}
.tm__grid {
gap: 4px;
}
.tm__head {
padding: 4px 8px;
gap: 8px;
}
.tm__avatar {
width: 20px;
height: 20px;
font-size: 9px;
}
.tm__meta b {
font-size: 10px;
}
.tm__meta i {
font-size: 9px;
}
.tm__bio p {
font-size: 9px;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.tm__card.is-open .tm__bio p {
padding: 0 8px 4px;
}
}// 실제 동작: 카드를 클릭하거나 Enter/Space 를 누르면 aria-expanded 가 바뀌며 그 카드의 한줄 소개가 grid-template-rows 로 실제로 펼쳐지고, 다시 누르면 접힌다.
// .is-demo 자동 Play: 카드를 하나씩 돌아가며 자동으로 펼쳐 보여준다(900ms 마다).
var cards = document.querySelectorAll('.tm__card');
function openOnly(idx) {
cards.forEach(function (c, n) {
var on = n === idx;
c.classList.toggle('is-open', on);
c.querySelector('.tm__head').setAttribute('aria-expanded', String(on));
});
}
cards.forEach(function (card, idx) {
card.querySelector('.tm__head').addEventListener('click', function () {
var open = card.classList.contains('is-open');
openOnly(open ? -1 : idx);
restart();
});
});
var ti = 0, timer = null;
function tick() { ti = (ti + 1) % cards.length; openOnly(ti); }
function restart() { if (timer) clearInterval(timer); timer = setInterval(tick, 900); }
openOnly(0);
restart();import { useEffect, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./TeamMembersGrid.module.scss";
export type TeamMembersGridProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const MEMBERS = [
{ initial: "이", name: "이서윤", role: "제품 디자인", bio: "3년째 컴포넌트 라이브러리를 다듬고 있어요. 커피보다 그리드를 더 좋아해요." },
{ initial: "박", name: "박도현", role: "프런트엔드", bio: "접근성 없는 인터랙션은 안 만든다는 원칙으로 일해요." },
{ initial: "최", name: "최하은", role: "고객 성공", bio: "도입 첫 주를 가장 중요하게 생각해요. 질문은 언제나 환영이에요." },
];
export default function TeamMembersGrid({ duration = 600, easing = "cubic-bezier(.2,.8,.2,1)", color = "#ff9ebb" }: TeamMembersGridProps) {
const [open, setOpen] = useState<number | null>(0);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
// 자동 재생: 900ms 마다 다음 멤버를 돌아가며 펼쳐 보여준다
useEffect(() => {
const id = setInterval(() => setOpen((o) => ((o ?? -1) + 1) % MEMBERS.length), 900);
return () => clearInterval(id);
}, []);
return (
<section className={styles.tm} style={vars}>
<p className={styles.title}>
<span className={styles.ic} aria-hidden="true">
<svg viewBox="0 0 256 256" width="18" height="18">
<g fill="currentColor">
<path d="M168 144a40 40 0 1 1-40-40a40 40 0 0 1 40 40M64 56a32 32 0 1 0 32 32a32 32 0 0 0-32-32m128 0a32 32 0 1 0 32 32a32 32 0 0 0-32-32" opacity=".2" />
<path d="M244.8 150.4a8 8 0 0 1-11.2-1.6A51.6 51.6 0 0 0 192 128a8 8 0 0 1 0-16a24 24 0 1 0-23.24-30a8 8 0 1 1-15.5-4A40 40 0 1 1 219 117.51a67.94 67.94 0 0 1 27.43 21.68a8 8 0 0 1-1.63 11.21M190.92 212a8 8 0 1 1-13.85 8a57 57 0 0 0-98.15 0a8 8 0 1 1-13.84-8a72.06 72.06 0 0 1 33.74-29.92a48 48 0 1 1 58.36 0A72.06 72.06 0 0 1 190.92 212M128 176a32 32 0 1 0-32-32a32 32 0 0 0 32 32m-56-56a8 8 0 0 0-8-8a24 24 0 1 1 23.24-30a8 8 0 1 0 15.5-4A40 40 0 1 0 37 117.51a67.94 67.94 0 0 0-27.4 21.68a8 8 0 1 0 12.8 9.61A51.6 51.6 0 0 1 64 128a8 8 0 0 0 8-8" />
</g>
</svg>
</span>
팀 소개
</p>
<ul className={styles.grid}>
{MEMBERS.map((m, i) => {
const isOpen = open === i;
return (
<li key={m.name} className={`${styles.card} ${isOpen ? styles.isOpen : ""}`}>
<button type="button" className={styles.head} aria-expanded={isOpen} onClick={() => setOpen(isOpen ? null : i)}>
<span className={styles.avatar}>{m.initial}</span>
<span className={styles.meta}><b>{m.name}</b><i>{m.role}</i></span>
</button>
<div className={styles.bio}><p>{m.bio}</p></div>
</li>
);
})}
</ul>
</section>
);
}04 Team members grid — Measured area 35.494% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · 9 Landing Page Sections That Build Trust
The post this demo lives in
9 Landing Page Sections That Build TrustLanding page sections like these are the trust-building pieces that usually end up scattered across templates. Nine go h See all demos in this post


