07로드맵 타임라인
1.6초마다 스포트라이트가 다음 분기로 자동으로 옮겨가고, 항목을 클릭하거나 포커스하면 실측한 위치로 translateY 로 바로 이동합니다.
쓰임제품 로드맵 섹션, 회사소개 페이지 연혁·분기별 계획
setIntervalgetBoundingClientRecttranslateY

<main class="stage">
<ol class="rm">
<li class="rm__step" tabindex="0">
<span class="rm__marker" aria-hidden="true"><svg viewBox="0 0 256 256" width="16" height="16"><g fill="currentColor"><path d="M216 48v40H40V48a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8" opacity=".2"/><path d="M208 32h-24v-8a8 8 0 0 0-16 0v8H88v-8a8 8 0 0 0-16 0v8H48a16 16 0 0 0-16 16v160a16 16 0 0 0 16 16h160a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16M72 48v8a8 8 0 0 0 16 0v-8h80v8a8 8 0 0 0 16 0v-8h24v32H48V48Zm136 160H48V96h160zm-38.34-85.66a8 8 0 0 1 0 11.32l-48 48a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L116 164.69l42.34-42.35a8 8 0 0 1 11.32 0"/></g></svg></span>
<p class="rm__q">1분기</p><p class="rm__t">컴포넌트 라이브러리 공개</p>
</li>
<li class="rm__step" tabindex="0">
<span class="rm__marker" aria-hidden="true"><svg viewBox="0 0 256 256" width="16" height="16"><g fill="currentColor"><path d="M216 48v40H40V48a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8" opacity=".2"/><path d="M208 32h-24v-8a8 8 0 0 0-16 0v8H88v-8a8 8 0 0 0-16 0v8H48a16 16 0 0 0-16 16v160a16 16 0 0 0 16 16h160a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16M72 48v8a8 8 0 0 0 16 0v-8h80v8a8 8 0 0 0 16 0v-8h24v32H48V48Zm136 160H48V96h160zm-38.34-85.66a8 8 0 0 1 0 11.32l-48 48a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L116 164.69l42.34-42.35a8 8 0 0 1 11.32 0"/></g></svg></span>
<p class="rm__q">2분기</p><p class="rm__t">i18n(영어·일본어) 지원</p>
</li>
<li class="rm__step" tabindex="0">
<span class="rm__marker" aria-hidden="true"><svg viewBox="0 0 256 256" width="16" height="16"><g fill="currentColor"><path d="M184 120v61.65a8 8 0 0 1-2.34 5.65l-34.35 34.35a8 8 0 0 1-13.57-4.53L128 176Zm-48-48H74.35a8 8 0 0 0-5.65 2.34l-34.35 34.35a8 8 0 0 0 4.53 13.57L80 128ZM40 216c37.65 0 50.69-19.69 54.56-28.18l-26.38-26.38C59.69 165.31 40 178.35 40 216" opacity=".2"/><path d="M223.85 47.12a16 16 0 0 0-15-15c-12.58-.75-44.73.4-71.41 27.07L132.69 64H74.36A15.9 15.9 0 0 0 63 68.68L28.7 103a16 16 0 0 0 9.07 27.16l38.47 5.37l44.21 44.21l5.37 38.49a15.94 15.94 0 0 0 10.78 12.92a16.1 16.1 0 0 0 5.1.83a15.9 15.9 0 0 0 11.3-4.68l34.32-34.3a15.9 15.9 0 0 0 4.68-11.36v-58.33l4.77-4.77c26.68-26.68 27.83-58.83 27.08-71.42M74.36 80h42.33l-39.53 39.52L40 114.34Zm74.41-9.45a76.65 76.65 0 0 1 59.11-22.47a76.46 76.46 0 0 1-22.42 59.16L128 164.68L91.32 128ZM176 181.64L141.67 216l-5.19-37.17L176 139.31Zm-74.16 9.5C97.34 201 82.29 224 40 224a8 8 0 0 1-8-8c0-42.29 23-57.34 32.86-61.85a8 8 0 0 1 6.64 14.56c-6.43 2.93-20.62 12.36-23.12 38.91c26.55-2.5 36-16.69 38.91-23.12a8 8 0 1 1 14.56 6.64Z"/></g></svg></span>
<p class="rm__q">3분기</p><p class="rm__t">React 패키지 출시</p>
</li>
<li class="rm__step" tabindex="0">
<span class="rm__marker" aria-hidden="true"><svg viewBox="0 0 256 256" width="16" height="16"><g fill="currentColor"><path d="M184 120v61.65a8 8 0 0 1-2.34 5.65l-34.35 34.35a8 8 0 0 1-13.57-4.53L128 176Zm-48-48H74.35a8 8 0 0 0-5.65 2.34l-34.35 34.35a8 8 0 0 0 4.53 13.57L80 128ZM40 216c37.65 0 50.69-19.69 54.56-28.18l-26.38-26.38C59.69 165.31 40 178.35 40 216" opacity=".2"/><path d="M223.85 47.12a16 16 0 0 0-15-15c-12.58-.75-44.73.4-71.41 27.07L132.69 64H74.36A15.9 15.9 0 0 0 63 68.68L28.7 103a16 16 0 0 0 9.07 27.16l38.47 5.37l44.21 44.21l5.37 38.49a15.94 15.94 0 0 0 10.78 12.92a16.1 16.1 0 0 0 5.1.83a15.9 15.9 0 0 0 11.3-4.68l34.32-34.3a15.9 15.9 0 0 0 4.68-11.36v-58.33l4.77-4.77c26.68-26.68 27.83-58.83 27.08-71.42M74.36 80h42.33l-39.53 39.52L40 114.34Zm74.41-9.45a76.65 76.65 0 0 1 59.11-22.47a76.46 76.46 0 0 1-22.42 59.16L128 164.68L91.32 128ZM176 181.64L141.67 216l-5.19-37.17L176 139.31Zm-74.16 9.5C97.34 201 82.29 224 40 224a8 8 0 0 1-8-8c0-42.29 23-57.34 32.86-61.85a8 8 0 0 1 6.64 14.56c-6.43 2.93-20.62 12.36-23.12 38.91c26.55-2.5 36-16.69 38.91-23.12a8 8 0 1 1 14.56 6.64Z"/></g></svg></span>
<p class="rm__q">4분기</p><p class="rm__t">셀프 호스팅 지원</p>
</li>
<li class="rm__spot" aria-hidden="true"></li>
</ol>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-lilac;
@include stage($stage-paper);
.rm { position: relative; width: min(280px, 92vw); list-style: none; margin: 0; padding: 0 0 0 $sp-8; }
.rm::before { content: ""; position: absolute; left: 14px; top: 4px; bottom: 4px; width: 2px; background: rgba(23, 20, 26, .12); }
.rm__step {
position: relative; padding: $sp-2 0; cursor: pointer;
outline: none;
}
.rm__step:focus-visible .rm__q { color: $subject-ink; }
.rm__marker {
position: absolute; left: -#{$sp-8}; top: $sp-2;
display: grid; place-items: center;
width: 28px; height: 28px; border-radius: 50%;
background: rgba(185, 160, 255, .18);
color: rgba(23, 20, 26, .4);
transition: color $duration $easing, background $duration $easing, transform $duration $easing;
}
.rm__step.is-active .rm__marker { background: $color; color: $stage-paper; transform: scale(1.08); }
.rm__q { margin: 0; font-weight: 800; font-size: 11px; line-height: 1.2; font-family: inherit; color: rgba(23, 20, 26, .72); transition: color $duration $easing; }
.rm__t { margin: $sp-1 0 0; font-weight: 600; font-size: 12px; line-height: 1.35; font-family: inherit; color: $subject-ink; }
.rm__step.is-active .rm__q { color: $subject-ink; } // 활성 분기 라벨 — 라일락 글자는 페이퍼 위 2.26:1
.rm__spot {
position: absolute; left: 0; top: 0;
width: 28px; height: 28px; margin-left: -#{$sp-8};
border-radius: 50%;
box-shadow: 0 0 0 3px rgba(185, 160, 255, .35);
transition: transform $duration $easing;
pointer-events: none;
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.rm__spot, .rm__marker, .rm__q { transition: none !important; }
}
// ── 반응형(320×200) ──────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.rm { padding-left: $sp-6; width: min(230px, 90vw); }
.rm::before { left: 8px; }
.rm__step { padding: $sp-1 0; }
.rm__marker { left: -#{$sp-6}; top: 3px; width: 18px; height: 18px; }
.rm__marker svg { width: 11px; height: 11px; }
.rm__q { font-size: 9px; }
.rm__t { font-size: 10px; line-height: 1.2; }
.rm__spot { width: 18px; height: 18px; margin-left: -#{$sp-6}; }
}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);
}
.rm {
position: relative;
width: min(280px, 92vw);
list-style: none;
margin: 0;
padding: 0 0 0 32px;
}
.rm::before {
content: "";
position: absolute;
left: 14px;
top: 4px;
bottom: 4px;
width: 2px;
background: rgba(23, 20, 26, 0.12);
}
.rm__step {
position: relative;
padding: 8px 0;
cursor: pointer;
outline: none;
}
.rm__step:focus-visible .rm__q {
color: #17141a;
}
.rm__marker {
position: absolute;
left: -32px;
top: 8px;
display: grid;
place-items: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: rgba(185, 160, 255, 0.18);
color: rgba(23, 20, 26, 0.4);
transition: color 300ms cubic-bezier(0.2, 0.8, 0.2, 1), background 300ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.rm__step.is-active .rm__marker {
background: #b9a0ff;
color: #ffffff;
transform: scale(1.08);
}
.rm__q {
margin: 0;
font-weight: 800;
font-size: 11px;
line-height: 1.2;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
transition: color 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.rm__t {
margin: 4px 0 0;
font-weight: 600;
font-size: 12px;
line-height: 1.35;
font-family: inherit;
color: #17141a;
}
.rm__step.is-active .rm__q {
color: #17141a;
}
.rm__spot {
position: absolute;
left: 0;
top: 0;
width: 28px;
height: 28px;
margin-left: -32px;
border-radius: 50%;
box-shadow: 0 0 0 3px rgba(185, 160, 255, 0.35);
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.rm__spot, .rm__marker, .rm__q {
transition: none !important;
}
}
@media (max-width: 340px), (max-height: 220px) {
.rm {
padding-left: 24px;
width: min(230px, 90vw);
}
.rm::before {
left: 8px;
}
.rm__step {
padding: 4px 0;
}
.rm__marker {
left: -24px;
top: 3px;
width: 18px;
height: 18px;
}
.rm__marker svg {
width: 11px;
height: 11px;
}
.rm__q {
font-size: 9px;
}
.rm__t {
font-size: 10px;
line-height: 1.2;
}
.rm__spot {
width: 18px;
height: 18px;
margin-left: -24px;
}
}// 실제 동작: 1.6초마다 스포트라이트가 자동으로 다음 분기로 옮겨가고, 항목을 클릭하거나 포커스하면(Tab) 그 자리로 실측한 위치만큼 translateY 로 바로 이동한다.
var steps = document.querySelectorAll('.rm__step');
var spot = document.querySelector('.rm__spot');
var list = document.querySelector('.rm');
var i = 0, timer = null;
function moveTo(idx) {
i = idx;
var top = steps[idx].getBoundingClientRect().top - list.getBoundingClientRect().top;
spot.style.transform = 'translateY(' + top + 'px)';
steps.forEach(function (s, n) { s.classList.toggle('is-active', n === idx); });
}
function tick() { moveTo((i + 1) % steps.length); }
function restart() { if (timer) clearInterval(timer); timer = setInterval(tick, 400); }
steps.forEach(function (s, idx) {
s.addEventListener('click', function () { moveTo(idx); restart(); });
s.addEventListener('focus', function () { moveTo(idx); restart(); });
});
moveTo(0);
restart();import { useEffect, useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./TimelineRoadmap.module.scss";
export type TimelineRoadmapProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const STEPS = [
{ q: "1분기", t: "컴포넌트 라이브러리 공개", done: true },
{ q: "2분기", t: "i18n(영어·일본어) 지원", done: true },
{ q: "3분기", t: "React 패키지 출시", done: false },
{ q: "4분기", t: "셀프 호스팅 지원", done: false },
];
function CalendarCheck() {
return (
<svg viewBox="0 0 256 256" width="16" height="16">
<g fill="currentColor">
<path d="M216 48v40H40V48a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8" opacity=".2" />
<path d="M208 32h-24v-8a8 8 0 0 0-16 0v8H88v-8a8 8 0 0 0-16 0v8H48a16 16 0 0 0-16 16v160a16 16 0 0 0 16 16h160a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16M72 48v8a8 8 0 0 0 16 0v-8h80v8a8 8 0 0 0 16 0v-8h24v32H48V48Zm136 160H48V96h160zm-38.34-85.66a8 8 0 0 1 0 11.32l-48 48a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L116 164.69l42.34-42.35a8 8 0 0 1 11.32 0" />
</g>
</svg>
);
}
function RocketLaunch() {
return (
<svg viewBox="0 0 256 256" width="16" height="16">
<g fill="currentColor">
<path d="M184 120v61.65a8 8 0 0 1-2.34 5.65l-34.35 34.35a8 8 0 0 1-13.57-4.53L128 176Zm-48-48H74.35a8 8 0 0 0-5.65 2.34l-34.35 34.35a8 8 0 0 0 4.53 13.57L80 128ZM40 216c37.65 0 50.69-19.69 54.56-28.18l-26.38-26.38C59.69 165.31 40 178.35 40 216" opacity=".2" />
<path d="M223.85 47.12a16 16 0 0 0-15-15c-12.58-.75-44.73.4-71.41 27.07L132.69 64H74.36A15.9 15.9 0 0 0 63 68.68L28.7 103a16 16 0 0 0 9.07 27.16l38.47 5.37l44.21 44.21l5.37 38.49a15.94 15.94 0 0 0 10.78 12.92a16.1 16.1 0 0 0 5.1.83a15.9 15.9 0 0 0 11.3-4.68l34.32-34.3a15.9 15.9 0 0 0 4.68-11.36v-58.33l4.77-4.77c26.68-26.68 27.83-58.83 27.08-71.42M74.36 80h42.33l-39.53 39.52L40 114.34Zm74.41-9.45a76.65 76.65 0 0 1 59.11-22.47a76.46 76.46 0 0 1-22.42 59.16L128 164.68L91.32 128ZM176 181.64L141.67 216l-5.19-37.17L176 139.31Zm-74.16 9.5C97.34 201 82.29 224 40 224a8 8 0 0 1-8-8c0-42.29 23-57.34 32.86-61.85a8 8 0 0 1 6.64 14.56c-6.43 2.93-20.62 12.36-23.12 38.91c26.55-2.5 36-16.69 38.91-23.12a8 8 0 1 1 14.56 6.64Z" />
</g>
</svg>
);
}
export default function TimelineRoadmap({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#b9a0ff" }: TimelineRoadmapProps) {
const [active, setActive] = useState(0);
const [top, setTop] = useState(0);
const listRef = useRef<HTMLOListElement>(null);
const stepRefs = useRef<(HTMLLIElement | null)[]>([]);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function moveTo(idx: number) {
setActive(idx);
const list = listRef.current;
const refs = stepRefs.current || [];
const step = refs[idx];
if (list && step) setTop(step.getBoundingClientRect().top - list.getBoundingClientRect().top);
}
useEffect(() => {
moveTo(0);
const id = setInterval(() => setActive((a) => (a + 1) % STEPS.length), 400);
return () => clearInterval(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => { moveTo(active); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [active]);
return (
<ol className={styles.rm} style={vars} ref={listRef}>
{STEPS.map((s, i) => (
<li
key={s.q}
ref={(el: HTMLLIElement | null) => { const refs = stepRefs.current; if (refs) refs[i] = el; }}
className={`${styles.step} ${active === i ? styles.isActive : ""}`}
tabIndex={0}
onClick={() => moveTo(i)}
onFocus={() => moveTo(i)}
>
<span className={styles.marker} aria-hidden="true">{s.done ? <CalendarCheck /> : <RocketLaunch />}</span>
<p className={styles.q}>{s.q}</p>
<p className={styles.t}>{s.t}</p>
</li>
))}
<li className={styles.spot} aria-hidden="true" style={{ transform: `translateY(${top}px)` }} />
</ol>
);
}07 로드맵 타임라인 — 측정 누적면적 4.467% · 움직인 프레임 6/23GODRICH 원작 · zip 에 SCSS·React 포함 · 믿음 가는 랜딩 페이지 섹션 템플릿 9종
이 데모가 속한 글
믿음 가는 랜딩 페이지 섹션 템플릿 9종랜딩 페이지 섹션 템플릿은 통계·로고 바·비교표부터 문의 폼까지, 랜딩 페이지 여기저기 흩어져 있던 신뢰 요소를 한 번에 붙일 수 있게 모은 코드 모음입니다. 랜딩 페이지에 바로 붙이는 아홉 개 중 하나는 숫자가 실 이 글의 데모 전부 보기


