9 Event Landing Page Templates — Copy-Paste
Event landing page templates are ready-to-paste hero, schedule, speaker, and pricing sections for conference and meetup pages, and one of the nine below counts
01 event-hero
02 event-schedule-timeline
03 event-speaker-grid
04 event-venue
05 event-ticket-pricing
06 event-sponsor-logo-bar
07 event-faq
08 event-register-cta
09 event-countdown
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Event hero
- 02 Schedule timeline
- 03 Speaker grid
- 04 Venue block
- 05 Ticket pricing
- 06 Sponsor logo bar
- 07 Event FAQ
- 08 Register CTA
- 09 Countdown band
The nine aren't ranked by popularity — they're ordered the way a visitor actually scrolls a landing page. A hero grabs attention first, a timeline sets expectations, a speaker grid builds trust, a venue block hands over logistics, a pricing table asks for a decision, a sponsor bar reinforces trust once more, an FAQ clears the last doubts, a register CTA converts, and a countdown band nails down why it has to happen now. Every photo, logo, and avatar placeholder below is a shape built from this site's own ink, blue, and cream tokens — nothing here is a real brand or a real face. Swap in an actual logo file or a photo and the layout, transitions, and JavaScript keep working exactly as they are, so the event landing page design survives the swap. For somewhere to collect the coverage once an event is over, the pressroom section templates follow on, and a panel showing attendance as numbers lives in the dashboard widget UI pieces.
01Event hero
A conference landing page hero
Three blurred circles drift behind the stage on a continuous translate-and-scale loop while the headline, date pill, and CTA row hold still in front of them.

<main class="stage">
<section class="eh">
<span class="eh__orb eh__orb--1" aria-hidden="true"></span>
<span class="eh__orb eh__orb--2" aria-hidden="true"></span>
<span class="eh__orb eh__orb--3" aria-hidden="true"></span>
<span class="eh__mark" aria-hidden="true"></span>
<p class="eh__eyebrow">CONFERENCE — 2026</p>
<h1 class="eh__title">For people who build products<br>day</h1>
<div class="eh__meta">
<span class="eh__pill">Oct 18</span>
<span class="eh__pill">Seoul · Offline</span>
</div>
<div class="eh__row">
<a class="eh__cta" href="#">Register now</a>
<button class="eh__save" type="button" aria-pressed="false">
<span class="eh__save-ic" aria-hidden="true"></span>
Notify me
</button>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-quick; // 관심 등록 버튼 전환
$easing: $ease-pop;
$color: $subject-blue; // CTA·아이콘 강조색
@include stage($stage-ink);
.eh {
position: relative;
box-sizing: border-box; // width 에 패딩을 포함 — 아니면 320px 에서 패딩만큼 넘친다(실측 2026-09-10)
width: min(400px, 92vw);
overflow: hidden;
border-radius: $r-card;
padding: $sp-3 $sp-5 $sp-2;
text-align: center;
isolation: isolate;
}
// ── 배경 원 세 개: 계속 떠다니는 장식(호버 아님, 무조건 재생) ──────────────
.eh__orb {
position: absolute;
border-radius: 50%;
filter: blur(18px);
opacity: .55;
z-index: -1;
}
.eh__orb--1 { width: 170px; height: 170px; left: -45px; top: -35px; background: radial-gradient(circle, $subject-blue, transparent 62%); opacity: 1; filter: blur(3px); animation: eh-float-a 2s ease-in-out infinite; }
.eh__orb--2 { width: 130px; height: 130px; right: -25px; top: 6px; background: radial-gradient(circle, $subject-cream, transparent 62%); opacity: .7; filter: blur(3px); animation: eh-float-b 2s ease-in-out infinite; }
.eh__orb--3 { width: 150px; height: 150px; left: 28%; bottom: -70px; background: radial-gradient(circle, $subject-blue, transparent 62%); opacity: .95; filter: blur(3px); animation: eh-float-a 2s ease-in-out infinite reverse; }
@keyframes eh-float-a {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate($sp-10, -$sp-8) scale(1.5); }
}
@keyframes eh-float-b {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(-$sp-8, $sp-8) scale(.6); }
}
// ── 플레이스홀더 워드마크: 실제 로고가 아니라 도형 하나 ──────────────
.eh__mark {
display: block;
width: $sp-5; height: $sp-5;
margin: 0 auto $sp-1;
background: $color;
border-radius: $r-xs;
transform: rotate(45deg);
}
.eh__eyebrow {
margin: 0 0 $sp-1;
font-weight: 700; font-size: 10px; line-height: 1; font-family: inherit;
letter-spacing: .14em;
color: rgba(255, 247, 230, .6);
}
.eh__title {
margin: 0 0 $sp-1;
font-weight: 800; font-size: clamp(16px, 4.6vw, 19px); line-height: 1.14; font-family: inherit;
color: $subject-cream;
}
.eh__meta { display: flex; justify-content: center; gap: $sp-2; flex-wrap: wrap; margin-bottom: $sp-1; }
.eh__pill {
padding: $sp-1 $sp-2;
border-radius: $r-pill;
background: rgba(255, 247, 230, .1);
color: rgba(255, 247, 230, .82);
font-weight: 600; font-size: clamp(10px, 2.8vw, 11px); line-height: 1; font-family: inherit;
}
.eh__row { display: flex; justify-content: center; align-items: center; gap: $sp-2; flex-wrap: wrap; }
.eh__cta {
padding: $sp-2 $sp-5;
border-radius: $r-control;
background: $subject-blue-deep;
color: $subject-cream;
font-weight: 700; font-size: 12px; line-height: 1.4; font-family: inherit;
text-decoration: none;
}
.eh__save {
display: inline-flex; align-items: center; gap: $sp-1;
padding: $sp-2 $sp-3;
border-radius: $r-control;
border: 1px solid rgba(255, 247, 230, .3);
background: transparent;
color: rgba(255, 247, 230, .82);
font-weight: 600; font-size: 12px; line-height: 1.4; font-family: inherit;
cursor: pointer;
transition: background $duration $easing, border-color $duration $easing, transform $duration $easing;
}
.eh__save-ic {
width: $sp-3; height: $sp-3;
border-radius: 0;
background: currentColor;
clip-path: polygon(50% 0%, 63% 38%, 100% 38%, 70% 60%, 82% 100%, 50% 76%, 18% 100%, 30% 60%, 0% 38%, 37% 38%);
opacity: .5;
transition: opacity $duration $easing, transform $duration $easing;
}
.eh__save[aria-pressed="true"] {
background: rgba(255, 247, 230, .12);
border-color: $subject-cream;
}
.eh__save[aria-pressed="true"] .eh__save-ic {
opacity: 1;
color: $color;
transform: scale(1.15);
}
// ── 반응형: 320×200 처럼 세로가 짧은 화면에선 장식을 줄인다(BD9) ──────────────
@media (max-width: 340px), (max-height: 220px) {
.eh { padding: $sp-2 $sp-4; }
.eh__mark { display: none; }
.eh__eyebrow { display: none; }
.eh__title { font-size: 14px; line-height: 1.15; margin-bottom: $sp-1; }
.eh__meta { margin-bottom: $sp-1; }
.eh__pill { padding: $sp-1 $sp-2; font-size: 9px; }
.eh__row { gap: $sp-1; }
.eh__cta, .eh__save { padding: 4px $sp-3; font-size: 10px; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.eh__orb { animation: none !important; }
.eh__save, .eh__save-ic { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.eh {
position: relative;
box-sizing: border-box;
width: min(400px, 92vw);
overflow: hidden;
border-radius: 16px;
padding: 12px 20px 8px;
text-align: center;
isolation: isolate;
}
.eh__orb {
position: absolute;
border-radius: 50%;
filter: blur(18px);
opacity: 0.55;
z-index: -1;
}
.eh__orb--1 {
width: 170px;
height: 170px;
left: -45px;
top: -35px;
background: radial-gradient(circle, #2f6df6, transparent 62%);
opacity: 1;
filter: blur(3px);
animation: eh-float-a 2s ease-in-out infinite;
}
.eh__orb--2 {
width: 130px;
height: 130px;
right: -25px;
top: 6px;
background: radial-gradient(circle, #fff7e6, transparent 62%);
opacity: 0.7;
filter: blur(3px);
animation: eh-float-b 2s ease-in-out infinite;
}
.eh__orb--3 {
width: 150px;
height: 150px;
left: 28%;
bottom: -70px;
background: radial-gradient(circle, #2f6df6, transparent 62%);
opacity: 0.95;
filter: blur(3px);
animation: eh-float-a 2s ease-in-out infinite reverse;
}
@keyframes eh-float-a {
0%, 100% {
transform: translate(0, 0) scale(1);
}
50% {
transform: translate(40px, -32px) scale(1.5);
}
}
@keyframes eh-float-b {
0%, 100% {
transform: translate(0, 0) scale(1);
}
50% {
transform: translate(-32px, 32px) scale(0.6);
}
}
.eh__mark {
display: block;
width: 20px;
height: 20px;
margin: 0 auto 4px;
background: #2f6df6;
border-radius: 4px;
transform: rotate(45deg);
}
.eh__eyebrow {
margin: 0 0 4px;
font-weight: 700;
font-size: 10px;
line-height: 1;
font-family: inherit;
letter-spacing: 0.14em;
color: rgba(255, 247, 230, 0.6);
}
.eh__title {
margin: 0 0 4px;
font-weight: 800;
font-size: clamp(16px, 4.6vw, 19px);
line-height: 1.14;
font-family: inherit;
color: #fff7e6;
}
.eh__meta {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 4px;
}
.eh__pill {
padding: 4px 8px;
border-radius: 999px;
background: rgba(255, 247, 230, 0.1);
color: rgba(255, 247, 230, 0.82);
font-weight: 600;
font-size: clamp(10px, 2.8vw, 11px);
line-height: 1;
font-family: inherit;
}
.eh__row {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.eh__cta {
padding: 8px 20px;
border-radius: 12px;
background: #0a4fe8;
color: #fff7e6;
font-weight: 700;
font-size: 12px;
line-height: 1.4;
font-family: inherit;
text-decoration: none;
}
.eh__save {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
border-radius: 12px;
border: 1px solid rgba(255, 247, 230, 0.3);
background: transparent;
color: rgba(255, 247, 230, 0.82);
font-weight: 600;
font-size: 12px;
line-height: 1.4;
font-family: inherit;
cursor: pointer;
transition: background 220ms cubic-bezier(0.2, 1.4, 0.4, 1), border-color 220ms cubic-bezier(0.2, 1.4, 0.4, 1), transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.eh__save-ic {
width: 12px;
height: 12px;
border-radius: 0;
background: currentColor;
clip-path: polygon(50% 0%, 63% 38%, 100% 38%, 70% 60%, 82% 100%, 50% 76%, 18% 100%, 30% 60%, 0% 38%, 37% 38%);
opacity: 0.5;
transition: opacity 220ms cubic-bezier(0.2, 1.4, 0.4, 1), transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.eh__save[aria-pressed=true] {
background: rgba(255, 247, 230, 0.12);
border-color: #fff7e6;
}
.eh__save[aria-pressed=true] .eh__save-ic {
opacity: 1;
color: #2f6df6;
transform: scale(1.15);
}
@media (max-width: 340px), (max-height: 220px) {
.eh {
padding: 8px 16px;
}
.eh__mark {
display: none;
}
.eh__eyebrow {
display: none;
}
.eh__title {
font-size: 14px;
line-height: 1.15;
margin-bottom: 4px;
}
.eh__meta {
margin-bottom: 4px;
}
.eh__pill {
padding: 4px 8px;
font-size: 9px;
}
.eh__row {
gap: 4px;
}
.eh__cta, .eh__save {
padding: 4px 12px;
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.eh__orb {
animation: none !important;
}
.eh__save, .eh__save-ic {
transition: none;
}
}// 실제 동작: 클릭마다 Notify me 상태가 실제로 바뀐다(아이콘 채움 + aria-pressed).
var btn = document.querySelector('.eh__save');
btn.addEventListener('click', function () {
var on = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!on));
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./EventHero.module.scss";
export type EventHeroProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function EventHero({ duration = 220, easing = "cubic-bezier(.2,1.4,.4,1)", color = "#2f6df6" }: EventHeroProps) {
const [saved, setSaved] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.eh} style={vars}>
<span className={`${styles.orb} ${styles.orb1}`} aria-hidden="true" />
<span className={`${styles.orb} ${styles.orb2}`} aria-hidden="true" />
<span className={styles.mark} aria-hidden="true" />
<p className={styles.eyebrow}>CONFERENCE — 2026</p>
<h1 className={styles.title}>제품을 만드는 사람들의 하루</h1>
<div className={styles.meta}>
<span className={styles.pill}>10월 18일</span>
<span className={styles.pill}>서울 · 오프라인</span>
</div>
<div className={styles.row}>
<a className={styles.cta} href="#">참가 신청</a>
<button
type="button"
className={styles.save}
aria-pressed={saved}
onClick={() => setSaved((v) => !v)}
>
<span className={styles.saveIc} aria-hidden="true" />
관심 등록
</button>
</div>
</section>
);
}01 · Event hero — Measured area 30.31% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password
A bookmark that really toggles
The "save this event" bookmark button on the right actually flips aria-pressed and fills its icon on every real click, so it fits a conference or meetup landing page, or the hero of a paid ad landing page pointed at one event.
.eh__orb--1 { animation: eh-float-a 2s ease-in-out infinite; }
@keyframes eh-float-a {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate($sp-10, -$sp-8) scale(1.5); }
}
.eh__save[aria-pressed="true"] {
background: rgba(255, 247, 230, .12);
border-color: $subject-cream;
}
.eh__save[aria-pressed="true"] .eh__save-ic {
opacity: 1;
color: $color;
transform: scale(1.15);
}
02Schedule timeline
Clicking fills the track
Clicking a stop on the vertical line fills the progress track up to that point with a real scaleY transform and rings the newly active dot with a box-shadow pulse, while an idle loop cycles the same four stops on its own until someone clicks.

<main class="stage">
<section class="tl is-demo" data-active="0">
<span class="tl__track" aria-hidden="true"><span class="tl__fill"></span></span>
<ol class="tl__list">
<li><button class="tl__stop" type="button" data-i="0" aria-current="step">
<span class="tl__dot" aria-hidden="true"></span>
<span class="tl__time">09:00</span><span class="tl__label">Check-in & coffee</span>
</button></li>
<li><button class="tl__stop" type="button" data-i="1">
<span class="tl__dot" aria-hidden="true"></span>
<span class="tl__time">10:00</span><span class="tl__label">Keynote</span>
</button></li>
<li><button class="tl__stop" type="button" data-i="2">
<span class="tl__dot" aria-hidden="true"></span>
<span class="tl__time">13:00</span><span class="tl__label">Networking lunch</span>
</button></li>
<li><button class="tl__stop" type="button" data-i="3">
<span class="tl__dot" aria-hidden="true"></span>
<span class="tl__time">15:00</span><span class="tl__label">Panel talk</span>
</button></li>
</ol>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 진행바가 채워지는 시간
$easing: $ease-spring;
$color: $subject-ink; // 진행바·점 색
@include stage($stage-yellow);
.tl {
position: relative;
width: min(300px, 88vw);
padding-left: $sp-6;
}
.tl__track {
position: absolute;
left: 5px; top: $sp-1; bottom: $sp-1;
width: 2px;
background: rgba(23, 20, 26, .16);
border-radius: $r-pill;
overflow: hidden;
}
.tl__fill {
position: absolute; inset: 0;
transform: scaleY(.25);
transform-origin: top; // 규칙8: 타임라인 세로 채움 위에서
background: $color;
transition: transform $duration $easing;
}
.tl__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: $sp-4; }
.tl__stop {
display: flex; align-items: center; gap: $sp-3;
background: none; border: 0; padding: 0;
text-align: left; cursor: pointer;
font: inherit; color: rgba($subject-ink, .72);
-webkit-tap-highlight-color: transparent;
}
.tl__dot {
width: $sp-2; height: $sp-2;
border-radius: 50%;
background: rgba(23, 20, 26, .3);
flex: 0 0 auto;
position: relative;
transition: background $duration $easing, transform $duration $easing;
}
.tl__time { font-weight: 700; font-size: clamp(11px, 3vw, 12px); line-height: 1; font-family: inherit; flex: 0 0 auto; }
.tl__label { font-weight: 600; font-size: clamp(12px, 3.4vw, 13px); line-height: 1.3; font-family: inherit; }
.tl__stop[aria-current="step"] { color: $subject-ink; }
.tl__stop[aria-current="step"] .tl__dot {
background: $color;
transform: scale(1.3);
box-shadow: 0 0 0 4px rgba(23, 20, 26, .14);
}
// ── 데모 루프: 진행바가 정류장 순서대로 흐르고, 그 순간의 점이 함께 펄스 ──────────────
.tl.is-demo .tl__fill { animation: tl-fill $dur-loop $easing infinite; }
@keyframes tl-fill {
0%, 8% { transform: scaleY(.25); }
25%, 33% { transform: scaleY(.5); }
50%, 58% { transform: scaleY(.75); }
75%, 83% { transform: scaleY(1); }
100% { transform: scaleY(.25); }
}
.tl.is-demo li:nth-child(1) .tl__dot { animation: tl-pulse $dur-loop $easing infinite; }
.tl.is-demo li:nth-child(2) .tl__dot { animation: tl-pulse $dur-loop $easing infinite; animation-delay: calc($dur-loop * .25); }
.tl.is-demo li:nth-child(3) .tl__dot { animation: tl-pulse $dur-loop $easing infinite; animation-delay: calc($dur-loop * .5); }
.tl.is-demo li:nth-child(4) .tl__dot { animation: tl-pulse $dur-loop $easing infinite; animation-delay: calc($dur-loop * .75); }
@keyframes tl-pulse {
0%, 92%, 100% { background: rgba(23, 20, 26, .3); transform: scale(1); box-shadow: 0 0 0 0 rgba(23, 20, 26, 0); }
4% { background: $color; transform: scale(1.3); box-shadow: 0 0 0 4px rgba(23, 20, 26, .14); }
20% { background: $color; transform: scale(1.3); box-shadow: 0 0 0 4px rgba(23, 20, 26, .14); }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.tl__fill, .tl__dot { transition: none; }
.tl.is-demo .tl__fill,
.tl.is-demo .tl__stop .tl__dot { 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: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.tl {
position: relative;
width: min(300px, 88vw);
padding-left: 24px;
}
.tl__track {
position: absolute;
left: 5px;
top: 4px;
bottom: 4px;
width: 2px;
background: rgba(23, 20, 26, 0.16);
border-radius: 999px;
overflow: hidden;
}
.tl__fill {
position: absolute;
inset: 0;
transform: scaleY(0.25);
transform-origin: top;
background: #17141a;
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.tl__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.tl__stop {
display: flex;
align-items: center;
gap: 12px;
background: none;
border: 0;
padding: 0;
text-align: left;
cursor: pointer;
font: inherit;
color: rgba(23, 20, 26, 0.72);
-webkit-tap-highlight-color: transparent;
}
.tl__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(23, 20, 26, 0.3);
flex: 0 0 auto;
position: relative;
transition: background 300ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.tl__time {
font-weight: 700;
font-size: clamp(11px, 3vw, 12px);
line-height: 1;
font-family: inherit;
flex: 0 0 auto;
}
.tl__label {
font-weight: 600;
font-size: clamp(12px, 3.4vw, 13px);
line-height: 1.3;
font-family: inherit;
}
.tl__stop[aria-current=step] {
color: #17141a;
}
.tl__stop[aria-current=step] .tl__dot {
background: #17141a;
transform: scale(1.3);
box-shadow: 0 0 0 4px rgba(23, 20, 26, 0.14);
}
.tl.is-demo .tl__fill {
animation: tl-fill 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes tl-fill {
0%, 8% {
transform: scaleY(0.25);
}
25%, 33% {
transform: scaleY(0.5);
}
50%, 58% {
transform: scaleY(0.75);
}
75%, 83% {
transform: scaleY(1);
}
100% {
transform: scaleY(0.25);
}
}
.tl.is-demo li:nth-child(1) .tl__dot {
animation: tl-pulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.tl.is-demo li:nth-child(2) .tl__dot {
animation: tl-pulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: 0.5s;
}
.tl.is-demo li:nth-child(3) .tl__dot {
animation: tl-pulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: 1s;
}
.tl.is-demo li:nth-child(4) .tl__dot {
animation: tl-pulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: 1.5s;
}
@keyframes tl-pulse {
0%, 92%, 100% {
background: rgba(23, 20, 26, 0.3);
transform: scale(1);
box-shadow: 0 0 0 0 rgba(23, 20, 26, 0);
}
4% {
background: #17141a;
transform: scale(1.3);
box-shadow: 0 0 0 4px rgba(23, 20, 26, 0.14);
}
20% {
background: #17141a;
transform: scale(1.3);
box-shadow: 0 0 0 4px rgba(23, 20, 26, 0.14);
}
}
@media (prefers-reduced-motion: reduce) {
.tl__fill, .tl__dot {
transition: none;
}
.tl.is-demo .tl__fill,
.tl.is-demo .tl__stop .tl__dot {
animation: none !important;
}
}// 실제 동작: 정류장을 클릭하면 진행바가 그 지점까지 실제로 채워지고 aria-current 가 옮겨간다.
var root = document.querySelector('.tl');
var stops = root.querySelectorAll('.tl__stop');
stops.forEach(function (btn) {
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
var i = btn.getAttribute('data-i');
root.setAttribute('data-active', i);
root.querySelector('.tl__fill').style.transform = 'scaleY(' + ((Number(i) + 1) / 4) + ')';
stops.forEach(function (s) { s.removeAttribute('aria-current'); });
btn.setAttribute('aria-current', 'step');
});
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./ScheduleTimeline.module.scss";
const STOPS = [
{ time: "09:00", label: "등록·웰컴 커피" },
{ time: "10:00", label: "기조연설" },
{ time: "13:00", label: "네트워킹 런치" },
{ time: "15:00", label: "패널 토론" },
];
export type ScheduleTimelineProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function ScheduleTimeline({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#17141a" }: ScheduleTimelineProps) {
const [active, setActive] = useState(0);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.tl} style={vars}>
<span className={styles.track} aria-hidden="true">
<span className={styles.fill} style={{ transform: `scaleY(${(active + 1) / STOPS.length})` }} />
</span>
<ol className={styles.list}>
{STOPS.map((s, i) => (
<li key={s.time}>
<button
type="button"
className={styles.stop}
aria-current={i === active ? "step" : undefined}
onClick={() => setActive(i)}
>
<span className={styles.dot} aria-hidden="true" />
<span className={styles.time}>{s.time}</span>
<span className={styles.label}>{s.label}</span>
</button>
</li>
))}
</ol>
</section>
);
}02 · Schedule timeline — Measured area 1.099% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
An idle loop until someone clicks
It works well for a same-day event agenda or a conference schedule block sitting under the hero.
.tl__fill {
position: absolute; inset: 0;
transform: scaleY(.25);
transform-origin: top;
background: $color;
transition: transform $duration $easing;
}
.tl__stop[aria-current="step"] .tl__dot {
background: $color;
transform: scale(1.3);
box-shadow: 0 0 0 4px rgba(23, 20, 26, .14);
}
03Speaker grid
Six cards tilting into place
Six cards fade and tilt into place one after another on load, each settling from a slight rotation back to flat.

<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">J. Kim</span>
<span class="sg__name">Minjun Kim</span>
<span class="sg__role">Product Designer · Team ○○</span>
</button><p class="sg__bio">How to design screens that need no explanation.</p></li>
<li class="sg__card"><button class="sg__toggle" type="button" aria-expanded="false">
<span class="sg__avatar" aria-hidden="true">S. Lee</span>
<span class="sg__name">Seoyeon Lee</span>
<span class="sg__role">Engineering Lead · ○○ Studio</span>
</button><p class="sg__bio">Ship fast, roll back before it's too late.</p></li>
<li class="sg__card"><button class="sg__toggle" type="button" aria-expanded="false">
<span class="sg__avatar" aria-hidden="true">D. Park</span>
<span class="sg__name">Doyoon Park</span>
<span class="sg__role">Growth Manager · ○○ Commerce</span>
</button><p class="sg__bio">Three user signals to check before the numbers.</p></li>
<li class="sg__card"><button class="sg__toggle" type="button" aria-expanded="false">
<span class="sg__avatar" aria-hidden="true">Y. Choi</span>
<span class="sg__name">Yuna Choi</span>
<span class="sg__role">Researcher · ○○ Lab</span>
</button><p class="sg__bio">What ten interviews show, what a hundred show.</p></li>
<li class="sg__card"><button class="sg__toggle" type="button" aria-expanded="false">
<span class="sg__avatar" aria-hidden="true">H. Jung</span>
<span class="sg__name">Haeun Jung</span>
<span class="sg__role">Founder · ○○</span>
</button><p class="sg__bio">Three failures, only the lessons, kept short.</p></li>
<li class="sg__card"><button class="sg__toggle" type="button" aria-expanded="false">
<span class="sg__avatar" aria-hidden="true">J. Han</span>
<span class="sg__name">Jiho Han</span>
<span class="sg__role">Platform Engineer · ○○</span>
</button><p class="sg__bio">When traffic spikes 10x, what breaks first?</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 · Speaker grid — Measured area 40.452% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password
One bio at a time
Clicking a card expands only that card's one-line bio with a max-height transition while the other five stay closed, which suits a speaker lineup, a team page, or a panel of judges.
.sg.is-demo .sg__card { animation: sg-in 2s $easing infinite; }
@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); }
}
.sg__card.is-open .sg__bio { max-height: 60px; opacity: 1; }
04Venue block
Three real tab buttons
Three real role="tab" buttons for subway, bus, and driving control which directions paragraph is visible, sliding an underline indicator with translateX and cross-fading the panel text with opacity.

<main class="stage">
<section class="vn is-demo">
<div class="vn__info">
<p class="vn__name">○○ Convention Center, 3F</p>
<p class="vn__addr">123 ○○-ro, Gangnam-gu, Seoul</p>
<div class="vn__tabs" role="tablist">
<button class="vn__tab" type="button" role="tab" aria-selected="true" data-i="0">Subway</button>
<button class="vn__tab" type="button" role="tab" aria-selected="false" data-i="1">Bus</button>
<button class="vn__tab" type="button" role="tab" aria-selected="false" data-i="2">Car</button>
<span class="vn__ind" aria-hidden="true"></span>
</div>
<div class="vn__panels">
<p class="vn__panel vn__panel--0">Line 2, ○○ Station Exit 3, 5 min walk.</p>
<p class="vn__panel vn__panel--1">Bus 000 or 0000, get off at ○○ Station.</p>
<p class="vn__panel vn__panel--2">Free parking for 2h, then ₩2,000/hr after.</p>
</div>
</div>
<div class="vn__map" aria-hidden="true">
<span class="vn__pin"></span>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 탭 인디케이터·패널 전환
$easing: $ease-spring;
$color: $subject-blue; // 인디케이터·핀 색
@include stage($stage-paper);
.vn {
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: $sp-4;
width: min(420px, 94vw);
align-items: stretch;
}
@media (max-width: 340px) {
.vn { grid-template-columns: 1fr; }
.vn__map { display: none; }
.vn__name { margin-bottom: $sp-1; font-size: 12px; }
.vn__addr { margin-bottom: $sp-2; font-size: 10px; }
.vn__tabs { margin-bottom: $sp-2; }
.vn__tab { padding: $sp-1 0; font-size: 10px; }
.vn__panel { font-size: 10px; }
}
.vn__name { margin: 0 0 $sp-1; font-weight: 700; font-size: clamp(12px, 3.4vw, 13px); line-height: 1.3; font-family: inherit; color: $subject-ink; }
.vn__addr { margin: 0 0 $sp-3; font-weight: 500; font-size: 11px; line-height: 1.4; font-family: inherit; color: rgba(23, 20, 26, .6); }
.vn__tabs {
position: relative;
display: flex;
border-bottom: 1px solid rgba(23, 20, 26, .12);
margin-bottom: $sp-3;
}
.vn__tab {
flex: 1 1 0;
padding: $sp-2 0;
background: none; border: 0; cursor: pointer;
font-weight: 600; font-size: 11px; line-height: 1; font-family: inherit;
color: rgba($subject-ink, .72);
-webkit-tap-highlight-color: transparent;
}
.vn__tab[aria-selected="true"] { color: $subject-ink; }
.vn__ind {
position: absolute; left: 0; bottom: -1px;
width: calc(100% / 3); height: 2px;
background: $color;
transform: translateX(0%);
transition: transform $duration $easing;
}
.vn__panels { position: relative; display: grid; min-height: 34px; }
.vn__panel {
grid-area: 1 / 1;
margin: 0;
font-weight: 500; font-size: 11px; line-height: 1.5; font-family: inherit;
color: rgba(23, 20, 26, .75);
opacity: 0;
transition: opacity $duration $easing;
}
.vn__panel--0 { opacity: 1; }
.vn__map {
position: relative;
border-radius: $r-card;
background:
linear-gradient(rgba(23, 20, 26, .08) 1px, transparent 1px) 0 0 / 20px 20px,
linear-gradient(90deg, rgba(23, 20, 26, .08) 1px, transparent 1px) 0 0 / 20px 20px,
$subject-cream;
min-height: 120px;
}
.vn__pin {
position: absolute; left: 46%; top: 42%;
width: $sp-3; height: $sp-3;
border-radius: 50%;
background: $color;
box-shadow: 0 0 0 0 rgba(47, 109, 246, .5);
animation: vn-ping 2s ease-out infinite;
}
@keyframes vn-ping {
0% { box-shadow: 0 0 0 0 rgba(47, 109, 246, .5); }
70% { box-shadow: 0 0 0 14px rgba(47, 109, 246, 0); }
100% { box-shadow: 0 0 0 14px rgba(47, 109, 246, 0); }
}
// ── 데모 루프: 클릭 전까지 탭 세 개를 스스로 오간다 ──────────────
.vn.is-demo .vn__ind { animation: vn-loop $dur-loop $easing infinite; }
.vn.is-demo .vn__panel--0 { animation: vn-panel-0 $dur-loop $easing infinite; }
.vn.is-demo .vn__panel--1 { animation: vn-panel-1 $dur-loop $easing infinite; }
.vn.is-demo .vn__panel--2 { animation: vn-panel-2 $dur-loop $easing infinite; }
@keyframes vn-loop {
0%, 15% { transform: translateX(0%); }
33%, 48% { transform: translateX(100%); }
66%, 81% { transform: translateX(200%); }
100% { transform: translateX(0%); }
}
@keyframes vn-panel-0 { 0%, 15% { opacity: 1; } 30%, 100% { opacity: 0; } }
@keyframes vn-panel-1 { 0%, 30% { opacity: 0; } 45%, 63% { opacity: 1; } 78%, 100% { opacity: 0; } }
@keyframes vn-panel-2 { 0%, 63% { opacity: 0; } 78%, 96% { opacity: 1; } 100% { opacity: 0; } }
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.vn__ind, .vn__panel { transition: none; }
.vn__pin { animation: none; }
.vn.is-demo .vn__ind,
.vn.is-demo .vn__panel--0,
.vn.is-demo .vn__panel--1,
.vn.is-demo .vn__panel--2 { 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: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.vn {
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: 16px;
width: min(420px, 94vw);
align-items: stretch;
}
@media (max-width: 340px) {
.vn {
grid-template-columns: 1fr;
}
.vn__map {
display: none;
}
.vn__name {
margin-bottom: 4px;
font-size: 12px;
}
.vn__addr {
margin-bottom: 8px;
font-size: 10px;
}
.vn__tabs {
margin-bottom: 8px;
}
.vn__tab {
padding: 4px 0;
font-size: 10px;
}
.vn__panel {
font-size: 10px;
}
}
.vn__name {
margin: 0 0 4px;
font-weight: 700;
font-size: clamp(12px, 3.4vw, 13px);
line-height: 1.3;
font-family: inherit;
color: #17141a;
}
.vn__addr {
margin: 0 0 12px;
font-weight: 500;
font-size: 11px;
line-height: 1.4;
font-family: inherit;
color: rgba(23, 20, 26, 0.6);
}
.vn__tabs {
position: relative;
display: flex;
border-bottom: 1px solid rgba(23, 20, 26, 0.12);
margin-bottom: 12px;
}
.vn__tab {
flex: 1 1 0;
padding: 8px 0;
background: none;
border: 0;
cursor: pointer;
font-weight: 600;
font-size: 11px;
line-height: 1;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
-webkit-tap-highlight-color: transparent;
}
.vn__tab[aria-selected=true] {
color: #17141a;
}
.vn__ind {
position: absolute;
left: 0;
bottom: -1px;
width: 33.3333333333%;
height: 2px;
background: #2f6df6;
transform: translateX(0%);
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.vn__panels {
position: relative;
display: grid;
min-height: 34px;
}
.vn__panel {
grid-area: 1/1;
margin: 0;
font-weight: 500;
font-size: 11px;
line-height: 1.5;
font-family: inherit;
color: rgba(23, 20, 26, 0.75);
opacity: 0;
transition: opacity 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.vn__panel--0 {
opacity: 1;
}
.vn__map {
position: relative;
border-radius: 16px;
background: linear-gradient(rgba(23, 20, 26, 0.08) 1px, transparent 1px) 0 0/20px 20px, linear-gradient(90deg, rgba(23, 20, 26, 0.08) 1px, transparent 1px) 0 0/20px 20px, #fff7e6;
min-height: 120px;
}
.vn__pin {
position: absolute;
left: 46%;
top: 42%;
width: 12px;
height: 12px;
border-radius: 50%;
background: #2f6df6;
box-shadow: 0 0 0 0 rgba(47, 109, 246, 0.5);
animation: vn-ping 2s ease-out infinite;
}
@keyframes vn-ping {
0% {
box-shadow: 0 0 0 0 rgba(47, 109, 246, 0.5);
}
70% {
box-shadow: 0 0 0 14px rgba(47, 109, 246, 0);
}
100% {
box-shadow: 0 0 0 14px rgba(47, 109, 246, 0);
}
}
.vn.is-demo .vn__ind {
animation: vn-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.vn.is-demo .vn__panel--0 {
animation: vn-panel-0 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.vn.is-demo .vn__panel--1 {
animation: vn-panel-1 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.vn.is-demo .vn__panel--2 {
animation: vn-panel-2 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes vn-loop {
0%, 15% {
transform: translateX(0%);
}
33%, 48% {
transform: translateX(100%);
}
66%, 81% {
transform: translateX(200%);
}
100% {
transform: translateX(0%);
}
}
@keyframes vn-panel-0 {
0%, 15% {
opacity: 1;
}
30%, 100% {
opacity: 0;
}
}
@keyframes vn-panel-1 {
0%, 30% {
opacity: 0;
}
45%, 63% {
opacity: 1;
}
78%, 100% {
opacity: 0;
}
}
@keyframes vn-panel-2 {
0%, 63% {
opacity: 0;
}
78%, 96% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.vn__ind, .vn__panel {
transition: none;
}
.vn__pin {
animation: none;
}
.vn.is-demo .vn__ind,
.vn.is-demo .vn__panel--0,
.vn.is-demo .vn__panel--1,
.vn.is-demo .vn__panel--2 {
animation: none !important;
}
}// 실제 동작: 탭을 클릭하면 그 이동수단 설명만 실제로 보인다.
var root = document.querySelector('.vn');
var tabs = root.querySelectorAll('.vn__tab');
tabs.forEach(function (t) {
t.addEventListener('click', function () {
root.classList.remove('is-demo');
tabs.forEach(function (o) { o.setAttribute('aria-selected', 'false'); });
t.setAttribute('aria-selected', 'true');
root.querySelector('.vn__ind').style.transform = 'translateX(' + (Number(t.dataset.i) * 100) + '%)';
root.querySelectorAll('.vn__panel').forEach(function (p, i) {
p.style.opacity = String(i === Number(t.dataset.i) ? 1 : 0);
});
});
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./VenueBlock.module.scss";
const DIRECTIONS = [
{ label: "지하철", text: "2호선 ○○역 3번 출구, 도보 5분." },
{ label: "버스", text: "간선 000·지선 0000번, ○○역 하차." },
{ label: "자차", text: "지하 주차장 2시간 무료, 이후 시간당 2천 원." },
];
export type VenueBlockProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function VenueBlock({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: VenueBlockProps) {
const [active, setActive] = useState(0);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.vn} style={vars}>
<div>
<p className={styles.name}>○○ 컨벤션센터 3층</p>
<p className={styles.addr}>서울 강남구 ○○로 123</p>
<div className={styles.tabs} role="tablist">
{DIRECTIONS.map((d, i) => (
<button
key={d.label}
type="button"
role="tab"
aria-selected={i === active}
className={styles.tab}
onClick={() => setActive(i)}
>
{d.label}
</button>
))}
<span className={styles.ind} style={{ transform: `translateX(${active * 100}%)` }} aria-hidden="true" />
</div>
<p className={styles.panel}>{DIRECTIONS[active].text}</p>
</div>
<div className={styles.map} aria-hidden="true">
<span className={styles.pin} />
</div>
</section>
);
}04 · Venue block — Measured area 2.01% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password
A pin that keeps rippling
A pin on the map placeholder keeps rippling in the background the whole time, which is exactly the shape a "getting here" section or a store-locator card needs.
tabs.forEach(function (t) {
t.addEventListener('click', function () {
tabs.forEach(function (o) { o.setAttribute('aria-selected', 'false'); });
t.setAttribute('aria-selected', 'true');
ind.style.transform = 'translateX(' + (Number(t.dataset.i) * 100) + '%)';
panels.forEach(function (p, i) {
p.style.opacity = String(i === Number(t.dataset.i) ? 1 : 0);
});
});
});
05Ticket pricing
A glow that never stops
The middle card carries a "popular" badge and a looping box-shadow glow that never stops, while the early-bird/regular role="switch" at the top cross-fades all three price figures at once with opacity the instant it's toggled.

<main class="stage">
<section class="tp is-demo">
<button class="tp__switch" type="button" role="switch" aria-checked="false">
<span class="tp__switch-label">Early Bird</span>
<span class="tp__switch-track" aria-hidden="true"><span class="tp__switch-knob"></span></span>
<span class="tp__switch-label">Regular</span>
</button>
<ul class="tp__cards">
<li class="tp__card">
<p class="tp__tier">Standard</p>
<p class="tp__price"><span class="tp__price-a">₩49,000</span><span class="tp__price-b">₩69,000</span></p>
<p class="tp__desc">All sessions · Booklet</p>
</li>
<li class="tp__card tp__card--pop">
<span class="tp__badge">Popular</span>
<p class="tp__tier">Premium</p>
<p class="tp__price"><span class="tp__price-a">₩89,000</span><span class="tp__price-b">₩119,000</span></p>
<p class="tp__desc">All sessions · Lunch</p>
</li>
<li class="tp__card">
<p class="tp__tier">VIP</p>
<p class="tp__price"><span class="tp__price-a">₩149,000</span><span class="tp__price-b">₩189,000</span></p>
<p class="tp__desc">All perks · Dinner</p>
</li>
</ul>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 스위치·가격 전환
$easing: $ease-spring;
$color: $subject-blue; // 스위치·인기 카드 강조색
@include stage($stage-ink);
.tp { width: min(420px, 94vw); }
.tp__switch {
display: flex; align-items: center; gap: $sp-2;
margin: 0 auto $sp-4;
background: none; border: 0; padding: 0;
cursor: pointer; font: inherit;
-webkit-tap-highlight-color: transparent;
}
.tp__switch-label { font-weight: 600; font-size: 11px; line-height: 1; font-family: inherit; color: rgba(255, 247, 230, .55); }
.tp__switch-track {
position: relative;
width: 34px; height: $sp-4;
border-radius: $r-pill;
background: rgba(255, 247, 230, .18);
}
.tp__switch-knob {
position: absolute; top: 2px; left: 2px;
width: 12px; height: 12px;
border-radius: 50%;
background: $subject-cream;
transform: translateX(0);
transition: transform $duration $easing;
}
.tp__switch[aria-checked="true"] .tp__switch-knob { transform: translateX(18px); background: $color; }
.tp__cards { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: $sp-2; }
@media (max-width: 340px) { .tp__cards { grid-template-columns: 1fr; } }
@media (max-width: 340px), (max-height: 220px) {
.tp__switch { margin-bottom: $sp-2; }
.tp__cards { grid-template-columns: repeat(3, 1fr); gap: $sp-1; }
.tp__card { padding: $sp-2 $sp-1; }
.tp__desc { display: none; }
.tp__tier { margin-bottom: $sp-1; font-size: 10px; }
}
.tp__card {
position: relative;
padding: $sp-4 $sp-2;
border-radius: $r-card;
background: rgba(255, 247, 230, .06);
border: 1px solid rgba(255, 247, 230, .12);
text-align: center;
}
.tp__card--pop {
background: rgba(47, 109, 246, .14);
border-color: $color;
animation: tp-glow 2.4s ease-in-out infinite;
}
@keyframes tp-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(47, 109, 246, .35); }
50% { box-shadow: 0 0 18px 2px rgba(47, 109, 246, .35); }
}
.tp__badge {
position: absolute; top: -10px; left: 50%; transform: translateX(-50%);
padding: $sp-1 $sp-2;
border-radius: $r-pill;
background: $subject-blue-deep;
color: $subject-cream;
font-weight: 700; font-size: 9px; line-height: 1.6; font-family: inherit;
}
.tp__tier { margin: 0 0 $sp-2; font-weight: 700; font-size: clamp(11px, 3vw, 12px); line-height: 1; font-family: inherit; color: $subject-cream; }
.tp__price { position: relative; margin: 0 0 $sp-1; min-height: 18px; }
.tp__price-a, .tp__price-b {
position: absolute; inset: 0;
font-weight: 800; font-size: clamp(12px, 3.4vw, 14px); line-height: 1; font-family: inherit;
color: $subject-cream;
transition: opacity $duration $easing;
}
.tp__price-b { opacity: 0; }
.tp__switch[aria-checked="true"] ~ .tp__cards .tp__price-a { opacity: 0; }
.tp__switch[aria-checked="true"] ~ .tp__cards .tp__price-b { opacity: 1; }
.tp__desc { margin: $sp-4 0 0; font-weight: 500; font-size: 9px; line-height: 1.4; font-family: inherit; color: rgba(255, 247, 230, .55); }
// ── 데모 루프: 클릭 전까지 얼리버드 ↔ 정가를 스스로 오간다 ──────────────
.tp.is-demo .tp__switch-knob { animation: tp-knob $dur-loop $easing infinite; }
.tp.is-demo .tp__price-a { animation: tp-a $dur-loop $easing infinite; }
.tp.is-demo .tp__price-b { animation: tp-b $dur-loop $easing infinite; }
@keyframes tp-knob {
0%, 40% { transform: translateX(0); background: $subject-cream; }
55%, 95% { transform: translateX(18px); background: $color; }
100% { transform: translateX(0); background: $subject-cream; }
}
@keyframes tp-a { 0%, 40% { opacity: 1; } 55%, 95% { opacity: 0; } 100% { opacity: 1; } }
@keyframes tp-b { 0%, 40% { opacity: 0; } 55%, 95% { opacity: 1; } 100% { opacity: 0; } }
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.tp__switch-knob, .tp__price-a, .tp__price-b { transition: none; }
.tp__card--pop { animation: none; }
.tp.is-demo .tp__switch-knob,
.tp.is-demo .tp__price-a,
.tp.is-demo .tp__price-b { 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: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.tp {
width: min(420px, 94vw);
}
.tp__switch {
display: flex;
align-items: center;
gap: 8px;
margin: 0 auto 16px;
background: none;
border: 0;
padding: 0;
cursor: pointer;
font: inherit;
-webkit-tap-highlight-color: transparent;
}
.tp__switch-label {
font-weight: 600;
font-size: 11px;
line-height: 1;
font-family: inherit;
color: rgba(255, 247, 230, 0.55);
}
.tp__switch-track {
position: relative;
width: 34px;
height: 16px;
border-radius: 999px;
background: rgba(255, 247, 230, 0.18);
}
.tp__switch-knob {
position: absolute;
top: 2px;
left: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff7e6;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.tp__switch[aria-checked=true] .tp__switch-knob {
transform: translateX(18px);
background: #2f6df6;
}
.tp__cards {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
@media (max-width: 340px) {
.tp__cards {
grid-template-columns: 1fr;
}
}
@media (max-width: 340px), (max-height: 220px) {
.tp__switch {
margin-bottom: 8px;
}
.tp__cards {
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}
.tp__card {
padding: 8px 4px;
}
.tp__desc {
display: none;
}
.tp__tier {
margin-bottom: 4px;
font-size: 10px;
}
}
.tp__card {
position: relative;
padding: 16px 8px;
border-radius: 16px;
background: rgba(255, 247, 230, 0.06);
border: 1px solid rgba(255, 247, 230, 0.12);
text-align: center;
}
.tp__card--pop {
background: rgba(47, 109, 246, 0.14);
border-color: #2f6df6;
animation: tp-glow 2.4s ease-in-out infinite;
}
@keyframes tp-glow {
0%, 100% {
box-shadow: 0 0 0 0 rgba(47, 109, 246, 0.35);
}
50% {
box-shadow: 0 0 18px 2px rgba(47, 109, 246, 0.35);
}
}
.tp__badge {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
padding: 4px 8px;
border-radius: 999px;
background: #0a4fe8;
color: #fff7e6;
font-weight: 700;
font-size: 9px;
line-height: 1.6;
font-family: inherit;
}
.tp__tier {
margin: 0 0 8px;
font-weight: 700;
font-size: clamp(11px, 3vw, 12px);
line-height: 1;
font-family: inherit;
color: #fff7e6;
}
.tp__price {
position: relative;
margin: 0 0 4px;
min-height: 18px;
}
.tp__price-a, .tp__price-b {
position: absolute;
inset: 0;
font-weight: 800;
font-size: clamp(12px, 3.4vw, 14px);
line-height: 1;
font-family: inherit;
color: #fff7e6;
transition: opacity 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.tp__price-b {
opacity: 0;
}
.tp__switch[aria-checked=true] ~ .tp__cards .tp__price-a {
opacity: 0;
}
.tp__switch[aria-checked=true] ~ .tp__cards .tp__price-b {
opacity: 1;
}
.tp__desc {
margin: 16px 0 0;
font-weight: 500;
font-size: 9px;
line-height: 1.4;
font-family: inherit;
color: rgba(255, 247, 230, 0.55);
}
.tp.is-demo .tp__switch-knob {
animation: tp-knob 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.tp.is-demo .tp__price-a {
animation: tp-a 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.tp.is-demo .tp__price-b {
animation: tp-b 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes tp-knob {
0%, 40% {
transform: translateX(0);
background: #fff7e6;
}
55%, 95% {
transform: translateX(18px);
background: #2f6df6;
}
100% {
transform: translateX(0);
background: #fff7e6;
}
}
@keyframes tp-a {
0%, 40% {
opacity: 1;
}
55%, 95% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes tp-b {
0%, 40% {
opacity: 0;
}
55%, 95% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.tp__switch-knob, .tp__price-a, .tp__price-b {
transition: none;
}
.tp__card--pop {
animation: none;
}
.tp.is-demo .tp__switch-knob,
.tp.is-demo .tp__price-a,
.tp.is-demo .tp__price-b {
animation: none !important;
}
}// 실제 동작: 스위치를 누르면 세 카드의 가격이 실제로 Early Bird/정가로 바뀐다.
var sw = document.querySelector('.tp__switch');
sw.addEventListener('click', function () {
document.querySelector('.tp').classList.remove('is-demo');
var on = sw.getAttribute('aria-checked') === 'true';
sw.setAttribute('aria-checked', String(!on));
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./TicketPricing.module.scss";
const TIERS = [
{ name: "스탠다드", early: "49,000원", regular: "69,000원", desc: "세션 전체 · 자료집", pop: false },
{ name: "프리미엄", early: "89,000원", regular: "119,000원", desc: "세션 전체 · 네트워킹 런치", pop: true },
{ name: "VIP", early: "149,000원", regular: "189,000원", desc: "전 혜택 · 연사 디너", pop: false },
];
export type TicketPricingProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function TicketPricing({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: TicketPricingProps) {
const [regular, setRegular] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.tp} style={vars}>
<button
type="button"
role="switch"
aria-checked={regular}
className={styles.switch}
onClick={() => setRegular((v) => !v)}
>
<span className={styles.switchLabel}>얼리버드</span>
<span className={styles.track} aria-hidden="true">
<span className={styles.knob} style={{ transform: regular ? "translateX(18px)" : "translateX(0)" }} />
</span>
<span className={styles.switchLabel}>정가</span>
</button>
<ul className={styles.cards}>
{TIERS.map((t) => (
<li key={t.name} className={t.pop ? `${styles.card} ${styles.pop}` : styles.card}>
{t.pop && <span className={styles.badge}>인기</span>}
<p className={styles.tier}>{t.name}</p>
<p className={styles.price}>{regular ? t.regular : t.early}</p>
<p className={styles.desc}>{t.desc}</p>
</li>
))}
</ul>
</section>
);
}05 · Ticket pricing — Measured area 4.375% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password
An instant price toggle
This is built for a ticket sales page or any tiered pricing comparison where the toggle needs to feel instant rather than reloaded.
.tp__price-a, .tp__price-b {
position: absolute; inset: 0;
transition: opacity $duration $easing;
}
.tp__price-b { opacity: 0; }
.tp__switch[aria-checked="true"] ~ .tp__cards .tp__price-a { opacity: 0; }
.tp__switch[aria-checked="true"] ~ .tp__cards .tp__price-b { opacity: 1; }
.tp__card--pop { animation: tp-glow 2.4s ease-in-out infinite; }
06Sponsor logo bar
A track duplicated end to end
Eight placeholder logo blocks — shapes, not real logos — sit in a track duplicated end to end so a translateX(-50%) loop scrolls forever with no visible seam, and hovering or tapping the pause button stops it in place.

<main class="stage">
<section class="sb" aria-label="Sponsor logos">
<p class="sb__label">SPONSORED BY</p>
<div class="sb__viewport">
<div class="sb__track">
<span class="sb__logo sb__logo--1" aria-hidden="true"></span>
<span class="sb__logo sb__logo--2" aria-hidden="true"></span>
<span class="sb__logo sb__logo--3" aria-hidden="true"></span>
<span class="sb__logo sb__logo--4" aria-hidden="true"></span>
<span class="sb__logo sb__logo--1" aria-hidden="true"></span>
<span class="sb__logo sb__logo--2" aria-hidden="true"></span>
<span class="sb__logo sb__logo--3" aria-hidden="true"></span>
<span class="sb__logo sb__logo--4" aria-hidden="true"></span>
</div>
</div>
<button class="sb__pause" type="button" aria-pressed="false">Pause</button>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: 14s; // 트랙 한 바퀴 시간 (근접값 아님 — 마퀴 전용 값)
$easing: linear;
$color: $subject-ink; // 로고 마크 색
@include stage($stage-yellow);
.sb { width: min(440px, 96vw); text-align: center; }
.sb__label {
margin: 0 0 $sp-3;
font-weight: 700; font-size: 10px; line-height: 1; font-family: inherit;
letter-spacing: .16em;
color: rgba($subject-ink, .72);
}
.sb__viewport {
overflow: hidden;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
// 트랙: gap 을 쓰지 않는다(BD15) — 항목마다 margin-right 로 간격을 준다
.sb__track {
display: flex;
width: max-content;
animation: sb-scroll $duration $easing infinite;
}
.sb.is-paused .sb__track,
.sb__viewport:hover .sb__track { animation-play-state: paused; }
@keyframes sb-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.sb__logo {
flex: 0 0 auto;
width: 64px; height: 32px;
margin-right: $sp-6;
border-radius: $r-sm;
background: rgba(23, 20, 26, .06);
display: flex; align-items: center; justify-content: center;
gap: $sp-1;
}
.sb__logo::before {
content: "";
width: 12px; height: 12px;
background: $color;
}
.sb__logo::after {
content: "";
width: 20px; height: 6px;
border-radius: $r-xs;
background: rgba(23, 20, 26, .28);
}
.sb__logo--1::before { border-radius: 50%; }
.sb__logo--2::before { border-radius: $r-xs; transform: rotate(45deg); }
.sb__logo--3::before { clip-path: polygon(50% 0%, 0% 100%, 100% 100%); border-radius: 0; }
.sb__logo--4::before { border-radius: $r-pill; width: 16px; }
.sb__pause {
margin-top: $sp-3;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
border: 1px solid rgba(23, 20, 26, .18);
background: none;
color: rgba($subject-ink, .72);
font-weight: 600; font-size: 10px; line-height: 1.6; font-family: inherit;
cursor: pointer;
}
.sb__pause[aria-pressed="true"] { background: rgba(23, 20, 26, .08); color: $subject-ink; }
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.sb__track { animation: none !important; transform: translateX(0); }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sb {
width: min(440px, 96vw);
text-align: center;
}
.sb__label {
margin: 0 0 12px;
font-weight: 700;
font-size: 10px;
line-height: 1;
font-family: inherit;
letter-spacing: 0.16em;
color: rgba(23, 20, 26, 0.72);
}
.sb__viewport {
overflow: hidden;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.sb__track {
display: flex;
width: max-content;
animation: sb-scroll 14s linear infinite;
}
.sb.is-paused .sb__track,
.sb__viewport:hover .sb__track {
animation-play-state: paused;
}
@keyframes sb-scroll {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
.sb__logo {
flex: 0 0 auto;
width: 64px;
height: 32px;
margin-right: 24px;
border-radius: 8px;
background: rgba(23, 20, 26, 0.06);
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.sb__logo::before {
content: "";
width: 12px;
height: 12px;
background: #17141a;
}
.sb__logo::after {
content: "";
width: 20px;
height: 6px;
border-radius: 4px;
background: rgba(23, 20, 26, 0.28);
}
.sb__logo--1::before {
border-radius: 50%;
}
.sb__logo--2::before {
border-radius: 4px;
transform: rotate(45deg);
}
.sb__logo--3::before {
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
border-radius: 0;
}
.sb__logo--4::before {
border-radius: 999px;
width: 16px;
}
.sb__pause {
margin-top: 12px;
padding: 4px 12px;
border-radius: 999px;
border: 1px solid rgba(23, 20, 26, 0.18);
background: none;
color: rgba(23, 20, 26, 0.72);
font-weight: 600;
font-size: 10px;
line-height: 1.6;
font-family: inherit;
cursor: pointer;
}
.sb__pause[aria-pressed=true] {
background: rgba(23, 20, 26, 0.08);
color: #17141a;
}
@media (prefers-reduced-motion: reduce) {
.sb__track {
animation: none !important;
transform: translateX(0);
}
}// 실제 동작: 버튼을 누르거나 마우스를 올리면 흐름이 실제로 멈춘다(터치 기기는 버튼으로).
var root = document.querySelector('.sb');
var btn = document.querySelector('.sb__pause');
btn.addEventListener('click', function () {
var on = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!on));
root.classList.toggle('is-paused', !on);
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./SponsorLogoBar.module.scss";
const SHAPES = ["circle", "diamond", "triangle", "pill"] as const;
export type SponsorLogoBarProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function SponsorLogoBar({ duration = 14000, easing = "linear", color = "#17141a" }: SponsorLogoBarProps) {
const [paused, setPaused] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const logos = [...SHAPES, ...SHAPES];
return (
<section className={styles.sb} style={vars} aria-label="스폰서 로고">
<p className={styles.label}>SPONSORED BY</p>
<div className={styles.viewport}>
<div className={paused ? `${styles.track} ${styles.paused}` : styles.track}>
{logos.map((shape, i) => (
<span key={i} className={`${styles.logo} ${styles[shape]}`} aria-hidden="true" />
))}
</div>
</div>
<button
type="button"
className={styles.pause}
aria-pressed={paused}
onClick={() => setPaused((v) => !v)}
>
흐름 멈추기
</button>
</section>
);
}06 · Sponsor logo bar — Measured area 8.057% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
Pausing in place
It's the standard spot for a sponsor row, a partner list, or a "featured in" media bar.
// the track itself skips gap — each logo gets margin-right instead
.sb__track { display: flex; width: max-content; animation: sb-scroll 14s linear infinite; }
.sb__logo { flex: 0 0 auto; margin-right: $sp-6; }
@keyframes sb-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
07Event FAQ
Only one question open
Only one of the three questions can be open at a time — clicking a question expands its answer with a grid-template-rows: 0fr → 1fr transition and rotates the chevron, and opening one closes whichever was open before it.

<main class="stage">
<section class="fq is-demo">
<div class="fq__item is-open" data-i="0">
<button class="fq__q" type="button" aria-expanded="true">
<span>Can I get a refund?</span>
<span class="fq__chevron" aria-hidden="true"></span>
</button>
<div class="fq__a-wrap"><p class="fq__a">Full refund up to 7 days before; 10% fee after.</p></div>
</div>
<div class="fq__item" data-i="1">
<button class="fq__q" type="button" aria-expanded="false">
<span>Can I join online?</span>
<span class="fq__chevron" aria-hidden="true"></span>
</button>
<div class="fq__a-wrap"><p class="fq__a">In-person only. Key sessions go up on video later.</p></div>
</div>
<div class="fq__item" data-i="2">
<button class="fq__q" type="button" aria-expanded="false">
<span>Is parking available?</span>
<span class="fq__chevron" aria-hidden="true"></span>
</button>
<div class="fq__a-wrap"><p class="fq__a">Free parking for 2h, then ₩2,000/hr after.</p></div>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 답변 칸이 펼쳐지는 시간
$easing: $ease-spring;
$color: $subject-ink; // 화살표·강조 색
@include stage($stage-orange);
.fq { width: min(400px, 94vw); display: flex; flex-direction: column; gap: $sp-2; }
.fq__item { background: $subject-cream; border-radius: $r-card; overflow: hidden; }
.fq__q {
width: 100%;
display: flex; justify-content: space-between; align-items: center; gap: $sp-2;
padding: $sp-3 $sp-4;
background: none; border: 0; cursor: pointer; text-align: left;
font-weight: 600; font-size: clamp(11px, 3.2vw, 12px); line-height: 1.3; font-family: inherit;
color: $subject-ink;
-webkit-tap-highlight-color: transparent;
}
.fq__chevron {
flex: 0 0 auto;
width: 7px; height: 7px;
border-right: 2px solid rgba(23, 20, 26, .5);
border-bottom: 2px solid rgba(23, 20, 26, .5);
transform: rotate(-45deg);
transition: transform $duration $easing;
}
.fq__item.is-open .fq__chevron { transform: rotate(45deg); }
.fq__a-wrap {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows $duration $easing;
}
.fq__item.is-open .fq__a-wrap { grid-template-rows: 1fr; }
.fq__a {
min-height: 0;
overflow: hidden;
margin: 0;
padding: 0 $sp-4 $sp-3;
font-weight: 500; font-size: 11px; line-height: 1.5; font-family: inherit;
color: rgba(23, 20, 26, .7);
}
// ── 데모 루프: 클릭 전까지 세 질문이 순서대로 스스로 열린다 ──────────────
.fq.is-demo .fq__item:nth-child(1) .fq__a-wrap { animation: fq-open-0 $dur-loop $easing infinite; }
.fq.is-demo .fq__item:nth-child(1) .fq__chevron { animation: fq-chev-0 $dur-loop $easing infinite; }
.fq.is-demo .fq__item:nth-child(2) .fq__a-wrap { animation: fq-open-1 $dur-loop $easing infinite; }
.fq.is-demo .fq__item:nth-child(2) .fq__chevron { animation: fq-chev-1 $dur-loop $easing infinite; }
.fq.is-demo .fq__item:nth-child(3) .fq__a-wrap { animation: fq-open-2 $dur-loop $easing infinite; }
.fq.is-demo .fq__item:nth-child(3) .fq__chevron { animation: fq-chev-2 $dur-loop $easing infinite; }
@keyframes fq-open-0 { 0%, 15% { grid-template-rows: 1fr; } 30%, 100% { grid-template-rows: 0fr; } }
@keyframes fq-open-1 { 0%, 30% { grid-template-rows: 0fr; } 45%, 63% { grid-template-rows: 1fr; } 78%, 100% { grid-template-rows: 0fr; } }
@keyframes fq-open-2 { 0%, 63% { grid-template-rows: 0fr; } 78%, 96% { grid-template-rows: 1fr; } 100% { grid-template-rows: 0fr; } }
@keyframes fq-chev-0 { 0%, 15% { transform: rotate(45deg); } 30%, 100% { transform: rotate(-45deg); } }
@keyframes fq-chev-1 { 0%, 30% { transform: rotate(-45deg); } 45%, 63% { transform: rotate(45deg); } 78%, 100% { transform: rotate(-45deg); } }
@keyframes fq-chev-2 { 0%, 63% { transform: rotate(-45deg); } 78%, 96% { transform: rotate(45deg); } 100% { transform: rotate(-45deg); } }
// ── 반응형: 세로가 짧은 화면에선 여백을 줄인다(BD9) ──────────────
@media (max-width: 340px), (max-height: 220px) {
.fq { gap: $sp-1; }
.fq__q { padding: $sp-2 $sp-3; font-size: 11px; }
.fq__a { padding: 0 $sp-3 $sp-2; font-size: 10px; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.fq__chevron, .fq__a-wrap { transition: none; }
.fq.is-demo .fq__item .fq__a-wrap,
.fq.is-demo .fq__item .fq__chevron { 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);
}
.fq {
width: min(400px, 94vw);
display: flex;
flex-direction: column;
gap: 8px;
}
.fq__item {
background: #fff7e6;
border-radius: 16px;
overflow: hidden;
}
.fq__q {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: none;
border: 0;
cursor: pointer;
text-align: left;
font-weight: 600;
font-size: clamp(11px, 3.2vw, 12px);
line-height: 1.3;
font-family: inherit;
color: #17141a;
-webkit-tap-highlight-color: transparent;
}
.fq__chevron {
flex: 0 0 auto;
width: 7px;
height: 7px;
border-right: 2px solid rgba(23, 20, 26, 0.5);
border-bottom: 2px solid rgba(23, 20, 26, 0.5);
transform: rotate(-45deg);
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.fq__item.is-open .fq__chevron {
transform: rotate(45deg);
}
.fq__a-wrap {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.fq__item.is-open .fq__a-wrap {
grid-template-rows: 1fr;
}
.fq__a {
min-height: 0;
overflow: hidden;
margin: 0;
padding: 0 16px 12px;
font-weight: 500;
font-size: 11px;
line-height: 1.5;
font-family: inherit;
color: rgba(23, 20, 26, 0.7);
}
.fq.is-demo .fq__item:nth-child(1) .fq__a-wrap {
animation: fq-open-0 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.fq.is-demo .fq__item:nth-child(1) .fq__chevron {
animation: fq-chev-0 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.fq.is-demo .fq__item:nth-child(2) .fq__a-wrap {
animation: fq-open-1 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.fq.is-demo .fq__item:nth-child(2) .fq__chevron {
animation: fq-chev-1 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.fq.is-demo .fq__item:nth-child(3) .fq__a-wrap {
animation: fq-open-2 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.fq.is-demo .fq__item:nth-child(3) .fq__chevron {
animation: fq-chev-2 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes fq-open-0 {
0%, 15% {
grid-template-rows: 1fr;
}
30%, 100% {
grid-template-rows: 0fr;
}
}
@keyframes fq-open-1 {
0%, 30% {
grid-template-rows: 0fr;
}
45%, 63% {
grid-template-rows: 1fr;
}
78%, 100% {
grid-template-rows: 0fr;
}
}
@keyframes fq-open-2 {
0%, 63% {
grid-template-rows: 0fr;
}
78%, 96% {
grid-template-rows: 1fr;
}
100% {
grid-template-rows: 0fr;
}
}
@keyframes fq-chev-0 {
0%, 15% {
transform: rotate(45deg);
}
30%, 100% {
transform: rotate(-45deg);
}
}
@keyframes fq-chev-1 {
0%, 30% {
transform: rotate(-45deg);
}
45%, 63% {
transform: rotate(45deg);
}
78%, 100% {
transform: rotate(-45deg);
}
}
@keyframes fq-chev-2 {
0%, 63% {
transform: rotate(-45deg);
}
78%, 96% {
transform: rotate(45deg);
}
100% {
transform: rotate(-45deg);
}
}
@media (max-width: 340px), (max-height: 220px) {
.fq {
gap: 4px;
}
.fq__q {
padding: 8px 12px;
font-size: 11px;
}
.fq__a {
padding: 0 12px 8px;
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.fq__chevron, .fq__a-wrap {
transition: none;
}
.fq.is-demo .fq__item .fq__a-wrap,
.fq.is-demo .fq__item .fq__chevron {
animation: none !important;
}
}// 실제 동작: 질문을 클릭하면 그 답변만 실제로 펼쳐지고 나머지는 닫힌다(단일 오픈).
var root = document.querySelector('.fq');
var items = root.querySelectorAll('.fq__item');
items.forEach(function (item) {
item.querySelector('.fq__q').addEventListener('click', function () {
root.classList.remove('is-demo');
var open = item.classList.contains('is-open');
items.forEach(function (o) {
o.classList.remove('is-open');
o.querySelector('.fq__q').setAttribute('aria-expanded', 'false');
});
if (!open) {
item.classList.add('is-open');
item.querySelector('.fq__q').setAttribute('aria-expanded', 'true');
}
});
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./EventFaq.module.scss";
const ITEMS = [
{ q: "티켓은 환불되나요?", a: "행사 7일 전까지 전액 환불, 이후엔 수수료 10%가 붙습니다." },
{ q: "온라인으로도 참가할 수 있나요?", a: "현장 참가만 가능합니다. 주요 세션은 이후 영상으로 공개됩니다." },
{ q: "주차는 지원되나요?", a: "건물 지하 주차장 2시간 무료, 이후 시간당 2천 원입니다." },
];
export type EventFaqProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function EventFaq({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#17141a" }: EventFaqProps) {
const [open, setOpen] = useState(0);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.fq} style={vars}>
{ITEMS.map((item, i) => (
<div key={item.q} className={i === open ? `${styles.item} ${styles.isOpen}` : styles.item}>
<button
type="button"
className={styles.q}
aria-expanded={i === open}
onClick={() => setOpen((o) => (o === i ? -1 : i))}
>
<span>{item.q}</span>
<span className={styles.chevron} aria-hidden="true" />
</button>
<div className={styles.aWrap}>
<p className={styles.a}>{item.a}</p>
</div>
</div>
))}
</div>
);
}07 · Event FAQ — Measured area 28.795% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password
Why the page never grows tall
Ticket, refund, and parking questions are exactly what this single-open accordion pattern is built to answer without the page growing tall. For a page that leads with finished work rather than an event, the project showcase section templates are that arrangement.
.fq__a-wrap {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows $duration $easing;
}
.fq__item.is-open .fq__a-wrap { grid-template-rows: 1fr; }
.fq__a { min-height: 0; overflow: hidden; }
08Register CTA
An event registration landing page template that really validates
Submitting the form runs a real regex check on the email address rather than pretending to validate.

<main class="stage">
<section class="rc is-demo">
<span class="rc__orb rc__orb--1" aria-hidden="true"></span>
<span class="rc__orb rc__orb--2" aria-hidden="true"></span>
<h2 class="rc__title">Register now and<br>get early bird perks</h2>
<p class="rc__sub">Registrants get a booklet and networking invite.</p>
<form class="rc__form" novalidate>
<input class="rc__input" type="email" placeholder="[email protected]" required>
<button class="rc__submit" type="submit">
<span class="rc__submit-label">Register</span>
<svg class="rc__check" viewBox="0 0 24 24" aria-hidden="true"><polyline points="5 13 10 18 19 7" /></svg>
</button>
</form>
<p class="rc__done" role="status"></p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 체크·버튼 전환
$easing: $ease-pop;
$color: $subject-blue; // 버튼·체크 색
@include stage($stage-paper);
.rc {
position: relative;
width: min(360px, 92vw);
text-align: center;
overflow: hidden;
isolation: isolate;
}
.rc__orb {
position: absolute; border-radius: 50%; filter: blur(20px); z-index: -1;
}
.rc__orb--1 { width: 140px; height: 140px; left: -30px; top: -20px; background: radial-gradient(circle, $subject-blue, transparent 70%); opacity: .16; animation: rc-drift 8s ease-in-out infinite; }
.rc__orb--2 { width: 120px; height: 120px; right: -20px; bottom: -30px; background: radial-gradient(circle, $subject-blue, transparent 70%); opacity: .12; animation: rc-drift 7s ease-in-out infinite reverse; }
@keyframes rc-drift {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate($sp-4, -$sp-3); }
}
.rc__title { margin: 0 0 $sp-2; font-weight: 800; font-size: clamp(17px, 5vw, 21px); line-height: 1.3; font-family: inherit; color: $subject-ink; }
.rc__sub { margin: 0 0 $sp-4; font-weight: 500; font-size: clamp(11px, 3vw, 12px); line-height: 1.5; font-family: inherit; color: rgba(23, 20, 26, .6); }
.rc__form { display: flex; gap: $sp-2; justify-content: center; flex-wrap: wrap; }
.rc__input {
flex: 1 1 160px;
max-width: 220px;
padding: $sp-3 $sp-4;
border-radius: $r-control;
border: 1px solid rgba(23, 20, 26, .18);
font-weight: 500; font-size: 12px; line-height: 1; font-family: inherit;
}
.rc__input.is-shake { animation: rc-shake $dur-quick $ease-out; }
@keyframes rc-shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.rc__submit {
display: inline-flex; align-items: center; gap: $sp-1;
padding: $sp-3 $sp-5;
border-radius: $r-control;
border: 0;
background: $subject-blue-deep;
color: $subject-cream;
font-weight: 700; font-size: 12px; line-height: 1; font-family: inherit;
cursor: pointer;
transition: transform $duration $easing;
}
.rc__check {
width: 14px; height: 14px;
fill: none;
stroke: currentColor;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 24;
stroke-dashoffset: 24;
opacity: 0;
transition: stroke-dashoffset $duration $easing, opacity $duration $easing;
}
.rc__form.is-done .rc__submit { transform: scale(1.04); }
.rc__form.is-done .rc__check { stroke-dashoffset: 0; opacity: 1; }
.rc__form.is-done .rc__submit-label::after { content: " 완료"; }
.rc__done { min-height: 14px; margin: $sp-2 0 0; font-weight: 600; font-size: 11px; line-height: 1.4; font-family: inherit; color: $color; }
// ── 데모 루프: 클릭 전까지 버튼이 맴돌고 체크가 그려졌다 지워진다 ──────────────
.rc.is-demo .rc__submit { animation: rc-pulse $dur-loop $easing infinite; }
.rc.is-demo .rc__check { animation: rc-check $dur-loop $easing infinite; }
@keyframes rc-pulse {
0%, 55%, 100% { transform: scale(1); }
70% { transform: scale(1.06); }
}
@keyframes rc-check {
0%, 55% { stroke-dashoffset: 24; opacity: 0; }
72%, 92% { stroke-dashoffset: 0; opacity: 1; }
100% { stroke-dashoffset: 24; opacity: 0; }
}
// ── 반응형: 세로가 짧은 화면에선 여백을 줄인다(BD9) ──────────────
@media (max-width: 340px), (max-height: 220px) {
.rc__title { font-size: 15px; margin-bottom: $sp-1; }
.rc__sub { margin-bottom: $sp-2; font-size: 10px; }
.rc__input { padding: $sp-2 $sp-3; }
.rc__submit { padding: $sp-2 $sp-4; }
.rc__done { min-height: 0; margin-top: $sp-1; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.rc__orb { animation: none !important; }
.rc__submit, .rc__check, .rc__input { transition: none; animation: none !important; }
.rc.is-demo .rc__submit, .rc.is-demo .rc__check { animation: none !important; }
}@charset "UTF-8";
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);
}
.rc {
position: relative;
width: min(360px, 92vw);
text-align: center;
overflow: hidden;
isolation: isolate;
}
.rc__orb {
position: absolute;
border-radius: 50%;
filter: blur(20px);
z-index: -1;
}
.rc__orb--1 {
width: 140px;
height: 140px;
left: -30px;
top: -20px;
background: radial-gradient(circle, #2f6df6, transparent 70%);
opacity: 0.16;
animation: rc-drift 8s ease-in-out infinite;
}
.rc__orb--2 {
width: 120px;
height: 120px;
right: -20px;
bottom: -30px;
background: radial-gradient(circle, #2f6df6, transparent 70%);
opacity: 0.12;
animation: rc-drift 7s ease-in-out infinite reverse;
}
@keyframes rc-drift {
0%, 100% {
transform: translate(0, 0);
}
50% {
transform: translate(16px, -12px);
}
}
.rc__title {
margin: 0 0 8px;
font-weight: 800;
font-size: clamp(17px, 5vw, 21px);
line-height: 1.3;
font-family: inherit;
color: #17141a;
}
.rc__sub {
margin: 0 0 16px;
font-weight: 500;
font-size: clamp(11px, 3vw, 12px);
line-height: 1.5;
font-family: inherit;
color: rgba(23, 20, 26, 0.6);
}
.rc__form {
display: flex;
gap: 8px;
justify-content: center;
flex-wrap: wrap;
}
.rc__input {
flex: 1 1 160px;
max-width: 220px;
padding: 12px 16px;
border-radius: 12px;
border: 1px solid rgba(23, 20, 26, 0.18);
font-weight: 500;
font-size: 12px;
line-height: 1;
font-family: inherit;
}
.rc__input.is-shake {
animation: rc-shake 220ms ease-out;
}
@keyframes rc-shake {
0%, 100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
.rc__submit {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 12px 20px;
border-radius: 12px;
border: 0;
background: #0a4fe8;
color: #fff7e6;
font-weight: 700;
font-size: 12px;
line-height: 1;
font-family: inherit;
cursor: pointer;
transition: transform 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.rc__check {
width: 14px;
height: 14px;
fill: none;
stroke: currentColor;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 24;
stroke-dashoffset: 24;
opacity: 0;
transition: stroke-dashoffset 300ms cubic-bezier(0.2, 1.4, 0.4, 1), opacity 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.rc__form.is-done .rc__submit {
transform: scale(1.04);
}
.rc__form.is-done .rc__check {
stroke-dashoffset: 0;
opacity: 1;
}
.rc__form.is-done .rc__submit-label::after {
content: " 완료";
}
.rc__done {
min-height: 14px;
margin: 8px 0 0;
font-weight: 600;
font-size: 11px;
line-height: 1.4;
font-family: inherit;
color: #2f6df6;
}
.rc.is-demo .rc__submit {
animation: rc-pulse 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.rc.is-demo .rc__check {
animation: rc-check 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
@keyframes rc-pulse {
0%, 55%, 100% {
transform: scale(1);
}
70% {
transform: scale(1.06);
}
}
@keyframes rc-check {
0%, 55% {
stroke-dashoffset: 24;
opacity: 0;
}
72%, 92% {
stroke-dashoffset: 0;
opacity: 1;
}
100% {
stroke-dashoffset: 24;
opacity: 0;
}
}
@media (max-width: 340px), (max-height: 220px) {
.rc__title {
font-size: 15px;
margin-bottom: 4px;
}
.rc__sub {
margin-bottom: 8px;
font-size: 10px;
}
.rc__input {
padding: 8px 12px;
}
.rc__submit {
padding: 8px 16px;
}
.rc__done {
min-height: 0;
margin-top: 4px;
}
}
@media (prefers-reduced-motion: reduce) {
.rc__orb {
animation: none !important;
}
.rc__submit, .rc__check, .rc__input {
transition: none;
animation: none !important;
}
.rc.is-demo .rc__submit, .rc.is-demo .rc__check {
animation: none !important;
}
}// 실제 동작: 이메일 형식을 검사해 성공/오류를 실제로 표시한다.
var form = document.querySelector('.rc__form');
var input = document.querySelector('.rc__input');
var done = document.querySelector('.rc__done');
form.addEventListener('submit', function (e) {
e.preventDefault();
document.querySelector('.rc').classList.remove('is-demo');
var ok = /^\S+@\S+\.\S+$/.test(input.value);
if (ok) {
form.classList.add('is-done');
done.textContent = 'Registered — we sent a confirmation email.';
} else {
input.classList.remove('is-shake');
void input.offsetWidth;
input.classList.add('is-shake');
done.textContent = 'Please enter a valid email address.';
}
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./RegisterCta.module.scss";
export type RegisterCtaProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function RegisterCta({ duration = 300, easing = "cubic-bezier(.2,1.4,.4,1)", color = "#2f6df6" }: RegisterCtaProps) {
const [email, setEmail] = useState("");
const [status, setStatus] = useState<"idle" | "error" | "done">("idle");
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function onSubmit(e: any) {
e.preventDefault();
const ok = /^\S+@\S+\.\S+$/.test(email);
setStatus(ok ? "done" : "error");
}
return (
<section className={styles.rc} style={vars}>
<h2 className={styles.title}>지금 등록하고<br />얼리버드 혜택 받기</h2>
<p className={styles.sub}>등록자에게 자료집과 네트워킹 초대장을 드립니다.</p>
<form className={styles.form} onSubmit={onSubmit}>
<input
className={status === "error" ? `${styles.input} ${styles.shake}` : styles.input}
type="email"
placeholder="[email protected]"
value={email}
onChange={(e: any) => setEmail(e.target.value)}
/>
<button type="submit" className={styles.submit}>
{status === "done" ? "등록 완료" : "등록하기"}
<svg className={styles.check} viewBox="0 0 24 24" aria-hidden="true" style={{ opacity: status === "done" ? 1 : 0 }}>
<polyline points="5 13 10 18 19 7" />
</svg>
</button>
</form>
<p className={styles.done} role="status">
{status === "done" ? "등록 완료 — 확인 메일을 보냈습니다." : status === "error" ? "올바른 이메일을 입력해 주세요." : ""}
</p>
</section>
);
}08 · Register CTA — Measured area 0.857% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password
Pass, fail, and what moves
A passing address draws a checkmark onto the button with stroke-dashoffset and swaps the label to "done," while a failing one shakes the input left and right with translateX — nothing about the input itself moves before that submit happens.
form.addEventListener('submit', function (e) {
e.preventDefault();
var ok = /^\S+@\S+\.\S+$/.test(input.value);
if (ok) {
form.classList.add('is-done');
done.textContent = 'Registered — check your inbox for confirmation.';
} else {
input.classList.remove('is-shake');
void input.offsetWidth;
input.classList.add('is-shake');
done.textContent = 'Enter a valid email address.';
}
});
09Countdown band
Counting to a real deadline
Four number cards count down to a real deadline set exactly 7 days, 4 hours, 20 minutes, and 15 seconds from the moment the page loads, updating every second through setInterval.

<main class="stage">
<section class="cd">
<span class="cd__glow" aria-hidden="true"></span>
<p class="cd__label">Until the conference starts</p>
<div class="cd__row" role="timer" aria-live="off">
<div class="cd__unit"><span class="cd__num" data-u="d">07</span><span class="cd__unit-label">D</span></div>
<span class="cd__sep">:</span>
<div class="cd__unit"><span class="cd__num" data-u="h">04</span><span class="cd__unit-label">H</span></div>
<span class="cd__sep">:</span>
<div class="cd__unit"><span class="cd__num" data-u="m">20</span><span class="cd__unit-label">M</span></div>
<span class="cd__sep">:</span>
<div class="cd__unit"><span class="cd__num cd__num--s" data-u="s">15</span><span class="cd__unit-label">S</span></div>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-quick; // 초 카드 틱 전환
$easing: $ease-pop;
$color: $subject-blue; // 숫자 카드 강조색
@include stage($stage-ink);
.cd {
position: relative;
box-sizing: border-box; // width 에 패딩을 포함 — 아니면 320px 에서 패딩만큼 넘친다(실측 2026-09-10)
width: min(380px, 94vw);
text-align: center;
padding: $sp-6 $sp-4;
isolation: isolate;
overflow: hidden;
}
.cd__glow {
position: absolute; inset: -20% -10%;
z-index: -1;
background: radial-gradient(circle, rgba(47, 109, 246, .3), transparent 70%);
animation: cd-glow 2.6s ease-in-out infinite;
}
@keyframes cd-glow {
0%, 100% { opacity: .5; transform: scale(1); }
50% { opacity: .9; transform: scale(1.08); }
}
.cd__label {
margin: 0 0 $sp-4;
font-weight: 700; font-size: 11px; line-height: 1; font-family: inherit;
letter-spacing: .1em;
color: rgba(255, 247, 230, .6);
}
.cd__row { display: flex; align-items: center; justify-content: center; gap: $sp-2; }
.cd__unit { display: flex; flex-direction: column; align-items: center; gap: $sp-1; }
.cd__num {
display: block;
min-width: 40px;
padding: $sp-2 $sp-1;
border-radius: $r-sm;
background: rgba(255, 247, 230, .08);
color: $subject-cream;
font: 800 clamp(18px, 5.6vw, 24px)/1 "Space Mono", monospace;
font-variant-numeric: tabular-nums;
}
.cd__num--s { background: rgba(47, 109, 246, .22); color: $subject-cream; animation: cd-tick 1s $easing infinite; transform-origin: center; }
@keyframes cd-tick {
0%, 55% { transform: translateY(0) scale(1); }
78% { transform: translateY(-3px) scale(1.1); }
100% { transform: translateY(0) scale(1); }
}
.cd__unit-label { font-weight: 600; font-size: 9px; line-height: 1; font-family: inherit; color: rgba(255, 247, 230, .5); }
.cd__sep { font-weight: 700; font-size: 16px; line-height: 1; font-family: inherit; color: rgba($subject-cream, .72); margin-top: -$sp-4; }
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.cd__glow, .cd__num--s { 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: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.cd {
position: relative;
box-sizing: border-box;
width: min(380px, 94vw);
text-align: center;
padding: 24px 16px;
isolation: isolate;
overflow: hidden;
}
.cd__glow {
position: absolute;
inset: -20% -10%;
z-index: -1;
background: radial-gradient(circle, rgba(47, 109, 246, 0.3), transparent 70%);
animation: cd-glow 2.6s ease-in-out infinite;
}
@keyframes cd-glow {
0%, 100% {
opacity: 0.5;
transform: scale(1);
}
50% {
opacity: 0.9;
transform: scale(1.08);
}
}
.cd__label {
margin: 0 0 16px;
font-weight: 700;
font-size: 11px;
line-height: 1;
font-family: inherit;
letter-spacing: 0.1em;
color: rgba(255, 247, 230, 0.6);
}
.cd__row {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.cd__unit {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.cd__num {
display: block;
min-width: 40px;
padding: 8px 4px;
border-radius: 8px;
background: rgba(255, 247, 230, 0.08);
color: #fff7e6;
font: 800 clamp(18px, 5.6vw, 24px)/1 "Space Mono", monospace;
font-variant-numeric: tabular-nums;
}
.cd__num--s {
background: rgba(47, 109, 246, 0.22);
color: #fff7e6;
animation: cd-tick 1s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
transform-origin: center;
}
@keyframes cd-tick {
0%, 55% {
transform: translateY(0) scale(1);
}
78% {
transform: translateY(-3px) scale(1.1);
}
100% {
transform: translateY(0) scale(1);
}
}
.cd__unit-label {
font-weight: 600;
font-size: 9px;
line-height: 1;
font-family: inherit;
color: rgba(255, 247, 230, 0.5);
}
.cd__sep {
font-weight: 700;
font-size: 16px;
line-height: 1;
font-family: inherit;
color: rgba(255, 247, 230, 0.72);
margin-top: -16px;
}
@media (prefers-reduced-motion: reduce) {
.cd__glow, .cd__num--s {
animation: none !important;
}
}// 실제 동작: 목표 시각(지금부터 정확히 7D 4시간 20M 15S 뒤)까지 1초마다 실제로 줄어든다.
var target = Date.now() + ((7 * 24 * 60 * 60) + (4 * 60 * 60) + (20 * 60) + 15) * 1000;
var els = {
d: document.querySelector('[data-u="d"]'),
h: document.querySelector('[data-u="h"]'),
m: document.querySelector('[data-u="m"]'),
s: document.querySelector('[data-u="s"]')
};
function pad(n) { return String(Math.max(0, n)).padStart(2, '0'); }
function tick() {
var diff = Math.max(0, target - Date.now());
var s = Math.floor(diff / 1000);
els.d.textContent = pad(Math.floor(s / 86400));
els.h.textContent = pad(Math.floor((s % 86400) / 3600));
els.m.textContent = pad(Math.floor((s % 3600) / 60));
els.s.textContent = pad(s % 60);
}
tick();
setInterval(tick, 1000);import { useEffect, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./CountdownBand.module.scss";
function pad(n: number) {
return String(Math.max(0, n)).padStart(2, "0");
}
export type CountdownBandProps = { duration?: number; easing?: string; color?: string; targetMs?: number };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function CountdownBand({
duration = 220,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#2f6df6",
targetMs = Date.now() + ((7 * 24 * 60 * 60) + (4 * 60 * 60) + (20 * 60) + 15) * 1000,
}: CountdownBandProps) {
const [left, setLeft] = useState(Math.max(0, targetMs - Date.now()));
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
useEffect(() => {
const id = setInterval(() => setLeft(Math.max(0, targetMs - Date.now())), 1000);
return () => clearInterval(id);
}, [targetMs]);
const s = Math.floor(left / 1000);
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
return (
<section className={styles.cd} style={vars}>
<span className={styles.glow} aria-hidden="true" />
<p className={styles.label}>컨퍼런스 시작까지</p>
<div className={styles.row} role="timer" aria-live="off">
<div className={styles.unit}><span className={styles.num}>{pad(d)}</span><span className={styles.unitLabel}>일</span></div>
<span className={styles.sep}>:</span>
<div className={styles.unit}><span className={styles.num}>{pad(h)}</span><span className={styles.unitLabel}>시</span></div>
<span className={styles.sep}>:</span>
<div className={styles.unit}><span className={styles.num}>{pad(m)}</span><span className={styles.unitLabel}>분</span></div>
<span className={styles.sep}>:</span>
<div className={styles.unit}><span className={`${styles.num} ${styles.numS}`}>{pad(sec)}</span><span className={styles.unitLabel}>초</span></div>
</div>
</section>
);
}09 · Countdown band — Measured area 16.861% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password
Why the seconds card nudges
The seconds card nudges up and scales slightly on every tick rather than flipping — a deliberate choice covered in the trap below — which makes it a good fit for a pre-event countdown band or an early-bird deadline banner.
var target = Date.now() + ((7 * 24 * 60 * 60) + (4 * 60 * 60) + (20 * 60) + 15) * 1000;
function tick() {
var diff = Math.max(0, target - Date.now());
var s = Math.floor(diff / 1000);
els.d.textContent = pad(Math.floor(s / 86400));
els.h.textContent = pad(Math.floor((s % 86400) / 3600));
els.m.textContent = pad(Math.floor((s % 3600) / 60));
els.s.textContent = pad(s % 60);
}
tick();
setInterval(tick, 1000);
Where it breaks — the trap
The 320x200 breakpoint
The real trap across all nine wasn't any one animation — it was the 320×200 breakpoint. At the full 480×300 canvas every section behaved, but five of the nine (speaker grid, venue block, ticket pricing, FAQ, register CTA) grew to 260–450px tall once the width dropped to 320px, blowing past the 200px height budget. Folding the speaker grid to two columns made it worse, not better: six cards at two per row become three rows, which is taller than three cards per row ever was. The fix that actually worked was keeping three columns everywhere and hiding or shrinking only secondary text — role labels, venue descriptions, ticket line items — inside a @media (max-width: 340px), (max-height: 220px) block.
A flip that went flat
The countdown seconds card also went through a real rewrite. The first version flipped it with rotateX(0deg → -90deg → 0deg) for a coin-flip effect, but without a perspective value on the parent, the card went completely flat exactly at the 90-degree midpoint — MDN's transform-style reference explains why a 3D rotation collapses to nothing without a perspective context, and a poster screenshot landing on that frame showed a flat dash instead of a digit. Swapping to a translateY + scale nudge removed the flat frame entirely while keeping a visible tick. A countdown left standing past its date is how any time-bound part ages — the dashboard components go stale the same way when no refresh cadence is set. The sponsor logo bar has its own version of this problem: give the duplicated track a gap instead of margin-right on each logo and the seam between the two copies comes up short by gap ÷ 2, so the infinite scroll visibly jumps once per loop. With both fixes in and the 320×200 breakpoint sorted, open the zip with 5ngtcz6h typed exactly as it appears here and all nine behave the way this page describes.
Accessibility
State survives reduced motion
Every one of the nine respects prefers-reduced-motion: reduce, but each keeps its actual state rather than just going still — the schedule fill stays at whatever point it reached, the FAQ answer stays open once expanded, and the countdown digits keep updating every second while only the seconds card's nudge and the background glow turn off.
Tabs built to the ARIA pattern
The venue block (04) is a real role="tablist" with role="tab" and live aria-selected, built to the keyboard behavior in the W3C ARIA Authoring Practices tabs pattern.
Status and timer regions
The register CTA's result text (08) sits in a role="status" region so a screen reader announces success or failure the moment the form submits, and the countdown (09) carries role="timer" with aria-live="off" so the once-a-second digit change never interrupts whatever else is being read. More templates and layout sections live on the template category hub, and what this site does and doesn't sell is on the about page.
FAQ
Can I drop real photos and a logo into these event landing page examples?
Yes — that's the point. Every photo, avatar, and logo across all nine is a CSS shape built from this site's own tokens, not a real image file, so swapping in an actual <img> or a brand's logo file is a straight replacement that doesn't touch the layout or the JavaScript. The color tokens in demo-tokens.scss can stay as they are or get swapped for a brand palette without breaking anything.
Does the countdown band actually count down, or is it just decoration?
It actually counts down. The target time is set to exactly 7 days, 4 hours, 20 minutes, and 15 seconds past the moment the page loads, and setInterval recalculates the remaining days, hours, minutes, and seconds every second from Date.now(). Swapping in a real event date is a one-line change to that target value — the React port takes the same number as a targetMs prop.
How different is the React version from the plain CSS one?
The behavior is identical. Active tab, open accordion item, and the toggled switch all live in useState instead of DOM attributes, and the three SCSS variables — duration, easing, color — come in as component props that get forwarded to CSS custom properties. The countdown component runs the same setInterval tick logic inside a useEffect with its own cleanup, so the two versions tick in lockstep.









