03登壇者グリッド
카드 여섯 장이 격자 안에서 순서대로 떠오르며 살짝 기울었다 펴지고, 카드를 클릭하면 그 카드만 아래로 한 줄짜리 소개가 max-height 0 → 60px 트랜지션으로 펼쳐집니다.
使いどころ
staggergrid-template-columnsclick

<main class="stage">
<ul class="sg is-demo">
<li class="sg__card"><button class="sg__toggle" type="button" aria-expanded="false">
<span class="sg__avatar" aria-hidden="true">キム・ミ</span>
<span class="sg__name">キム・ミンジュン</span>
<span class="sg__role">プロダクトデザイナー・○○チーム</span>
</button><p class="sg__bio">「説明のいらない画面」の作り方を発表します。</p></li>
<li class="sg__card"><button class="sg__toggle" type="button" aria-expanded="false">
<span class="sg__avatar" aria-hidden="true">イ・ソ</span>
<span class="sg__name">イ・ソヨン</span>
<span class="sg__role">エンジニアリングリード・○○スタジオ</span>
</button><p class="sg__bio">早く作り、手遅れになる前に戻す、デプロイの話。</p></li>
<li class="sg__card"><button class="sg__toggle" type="button" aria-expanded="false">
<span class="sg__avatar" aria-hidden="true">パク・ド</span>
<span class="sg__name">パク・ドユン</span>
<span class="sg__role">グロースマネージャー・○○コマース</span>
</button><p class="sg__bio">数字より先に見るべきユーザーシグナル3つ。</p></li>
<li class="sg__card"><button class="sg__toggle" type="button" aria-expanded="false">
<span class="sg__avatar" aria-hidden="true">チェ・ユ</span>
<span class="sg__name">チェ・ユナ</span>
<span class="sg__role">リサーチャー・○○ラボ</span>
</button><p class="sg__bio">10回のインタビューで見えるもの、100回で見えるもの。</p></li>
<li class="sg__card"><button class="sg__toggle" type="button" aria-expanded="false">
<span class="sg__avatar" aria-hidden="true">ジョン・ハ</span>
<span class="sg__name">ジョン・ハウン</span>
<span class="sg__role">創業者・○○</span>
</button><p class="sg__bio">3回失敗して学んだことだけ、短く話します。</p></li>
<li class="sg__card"><button class="sg__toggle" type="button" aria-expanded="false">
<span class="sg__avatar" aria-hidden="true">ハン・ジ</span>
<span class="sg__name">ハン・ジホ</span>
<span class="sg__role">プラットフォームエンジニア・○○</span>
</button><p class="sg__bio">トラフィックが10倍になった時、最初に落ちる場所は。</p></li>
</ul>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-enter; // 카드 들어오는 시간
$easing: $ease-pop;
$color: $subject-ink; // 아바타 배경색
@include stage($stage-orange);
.sg {
list-style: none; margin: 0; padding: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $sp-3;
width: min(420px, 94vw);
}
@media (max-width: 340px), (max-height: 220px) {
// 2열로 바꾸면 6장이 3행이 돼 더 길어진다 — 3열을 유지하고 카드 안쪽만 줄인다
.sg { gap: $sp-1; }
.sg__toggle { padding: $sp-1; gap: $sp-1; }
.sg__avatar { width: $sp-5; height: $sp-5; font-size: 9px; }
.sg__role { display: none; }
}
.sg__card {
background: $subject-cream;
border-radius: $r-card;
overflow: hidden;
}
.sg__toggle {
display: flex; flex-direction: column; align-items: center; gap: $sp-1;
width: 100%;
padding: $sp-2;
border: 0; background: none; cursor: pointer;
font: inherit; text-align: center;
-webkit-tap-highlight-color: transparent;
}
.sg__avatar {
width: $sp-6; height: $sp-6;
display: grid; place-items: center;
border-radius: 50%;
background: $color;
color: $subject-cream;
font-weight: 700; font-size: 10px; line-height: 1; font-family: inherit;
}
.sg__name { font-weight: 700; font-size: clamp(11px, 3vw, 12px); line-height: 1.2; font-family: inherit; color: $subject-ink; }
.sg__role { font-weight: 500; font-size: 9px; line-height: 1.3; font-family: inherit; color: rgba($subject-ink, .72); }
.sg__bio {
margin: 0;
padding: 0 $sp-2;
max-height: 0;
overflow: hidden;
opacity: 0;
font-weight: 500; font-size: 9px; line-height: 1.4; font-family: inherit;
color: rgba(23, 20, 26, .7);
transition: max-height $duration $easing, opacity $duration $easing, padding-bottom $duration $easing;
}
.sg__card.is-open .sg__bio {
max-height: 60px;
opacity: 1;
padding-bottom: $sp-3;
}
// ── 데모 루프: 카드가 순서대로 살짝 기울며 떠오르고, 두 번째 카드는 소개까지 펼쳤다 접는다 ──────────────
.sg.is-demo .sg__card { animation: sg-in $dur-loop $easing infinite; }
.sg.is-demo .sg__card:nth-child(1) { animation-delay: 0ms; }
.sg.is-demo .sg__card:nth-child(2) { animation-delay: 90ms; }
.sg.is-demo .sg__card:nth-child(3) { animation-delay: 180ms; }
.sg.is-demo .sg__card:nth-child(4) { animation-delay: 270ms; }
.sg.is-demo .sg__card:nth-child(5) { animation-delay: 360ms; }
.sg.is-demo .sg__card:nth-child(6) { animation-delay: 450ms; }
@keyframes sg-in {
0% { opacity: .7; transform: translateY(6px) rotate(-1.5deg); }
18% { opacity: 1; transform: translateY(0) rotate(0deg); }
86% { opacity: 1; transform: translateY(0) rotate(0deg); }
100% { opacity: .7; transform: translateY(6px) rotate(-1.5deg); }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.sg__bio { transition: none; }
.sg.is-demo .sg__card,
.sg.is-demo .sg__card .sg__bio { animation: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sg {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
width: min(420px, 94vw);
}
@media (max-width: 340px), (max-height: 220px) {
.sg {
gap: 4px;
}
.sg__toggle {
padding: 4px;
gap: 4px;
}
.sg__avatar {
width: 20px;
height: 20px;
font-size: 9px;
}
.sg__role {
display: none;
}
}
.sg__card {
background: #fff7e6;
border-radius: 16px;
overflow: hidden;
}
.sg__toggle {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
width: 100%;
padding: 8px;
border: 0;
background: none;
cursor: pointer;
font: inherit;
text-align: center;
-webkit-tap-highlight-color: transparent;
}
.sg__avatar {
width: 24px;
height: 24px;
display: grid;
place-items: center;
border-radius: 50%;
background: #17141a;
color: #fff7e6;
font-weight: 700;
font-size: 10px;
line-height: 1;
font-family: inherit;
}
.sg__name {
font-weight: 700;
font-size: clamp(11px, 3vw, 12px);
line-height: 1.2;
font-family: inherit;
color: #17141a;
}
.sg__role {
font-weight: 500;
font-size: 9px;
line-height: 1.3;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
}
.sg__bio {
margin: 0;
padding: 0 8px;
max-height: 0;
overflow: hidden;
opacity: 0;
font-weight: 500;
font-size: 9px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.7);
transition: max-height 600ms cubic-bezier(0.2, 1.4, 0.4, 1), opacity 600ms cubic-bezier(0.2, 1.4, 0.4, 1), padding-bottom 600ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.sg__card.is-open .sg__bio {
max-height: 60px;
opacity: 1;
padding-bottom: 12px;
}
.sg.is-demo .sg__card {
animation: sg-in 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.sg.is-demo .sg__card:nth-child(1) {
animation-delay: 0ms;
}
.sg.is-demo .sg__card:nth-child(2) {
animation-delay: 90ms;
}
.sg.is-demo .sg__card:nth-child(3) {
animation-delay: 180ms;
}
.sg.is-demo .sg__card:nth-child(4) {
animation-delay: 270ms;
}
.sg.is-demo .sg__card:nth-child(5) {
animation-delay: 360ms;
}
.sg.is-demo .sg__card:nth-child(6) {
animation-delay: 450ms;
}
@keyframes sg-in {
0% {
opacity: 0.7;
transform: translateY(6px) rotate(-1.5deg);
}
18% {
opacity: 1;
transform: translateY(0) rotate(0deg);
}
86% {
opacity: 1;
transform: translateY(0) rotate(0deg);
}
100% {
opacity: 0.7;
transform: translateY(6px) rotate(-1.5deg);
}
}
@media (prefers-reduced-motion: reduce) {
.sg__bio {
transition: none;
}
.sg.is-demo .sg__card,
.sg.is-demo .sg__card .sg__bio {
animation: none !important;
}
}// 실제 동작: 카드를 클릭하면 그 카드만 한 줄 소개가 실제로 펼쳐진다.
document.querySelectorAll('.sg__toggle').forEach(function (btn) {
btn.addEventListener('click', function () {
document.querySelector('.sg').classList.remove('is-demo');
var open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
btn.closest('.sg__card').classList.toggle('is-open', !open);
});
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./SpeakerGrid.module.scss";
const SPEAKERS = [
{ initials: "김민", name: "김민준", role: "프로덕트 디자이너 · ○○팀", bio: "\"설명이 필요 없는 화면\"을 만드는 법을 발표합니다." },
{ initials: "이서", name: "이서연", role: "엔지니어링 리드 · ○○ 스튜디오", bio: "빠르게 만들고 늦지 않게 되돌리는 배포 이야기." },
{ initials: "박도", name: "박도윤", role: "그로스 매니저 · ○○ 커머스", bio: "숫자보다 먼저 봐야 하는 사용자 신호 세 가지." },
{ initials: "최유", name: "최유나", role: "리서처 · ○○ 랩", bio: "인터뷰 열 번이면 보이는 것, 백 번이면 보이는 것." },
{ initials: "정하", name: "정하은", role: "창업자 · ○○", bio: "세 번 실패하고 배운 것만 골라 짧게 말합니다." },
{ initials: "한지", name: "한지호", role: "플랫폼 엔지니어 · ○○", bio: "트래픽 열 배가 몰릴 때 먼저 죽는 곳은 어디인가." },
];
export type SpeakerGridProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function SpeakerGrid({ duration = 600, easing = "cubic-bezier(.2,1.4,.4,1)", color = "#17141a" }: SpeakerGridProps) {
const [open, setOpen] = useState<number | null>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<ul className={styles.sg} style={vars}>
{SPEAKERS.map((s, i) => (
<li key={s.name} className={styles.card}>
<button
type="button"
className={styles.toggle}
aria-expanded={open === i}
onClick={() => setOpen((o) => (o === i ? null : i))}
>
<span className={styles.avatar} aria-hidden="true">{s.initials}</span>
<span className={styles.name}>{s.name}</span>
<span className={styles.role}>{s.role}</span>
</button>
<p className={styles.bio} style={{ maxHeight: open === i ? 60 : 0, opacity: open === i ? 1 : 0 }}>{s.bio}</p>
</li>
))}
</ul>
);
}03 登壇者グリッド — 計測 累積面積 40.452% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · 【コピペOK】カンファレンス LP セクション テンプレート9選
このデモが属する記事
【コピペOK】カンファレンス LP セクション テンプレート9選カンファレンス LP セクションテンプレートは、ヒーロー・スケジュール・登壇者紹介のようなイベントランディングページ用ブロックを、LPにそのまま貼れる形でまとめたコード集です。9つのうち1つは、実際の目標時刻まで秒単位で減っていくカウントダ この記事のデモをすべて見る

