06チェックアウトステッパー
カート・配送・決済・完了の4段階のうち終わった段階は円の数字がSVGチェックアイコンに変わりstroke-dashoffsetが減って描かれ、次の段階までの連結線がscaleXで塗りつぶされる。
使いどころECの決済フロー、予約フォームのステップ
stroke-dashoffsetscaleXSVG

<main class="stage">
<div class="ts06-steps is-demo">
<div class="ts06-step">
<span class="ts06-circle" aria-label="完了">
<svg class="ts06-check" viewBox="0 0 16 16" fill="none" aria-hidden="true"><polyline points="3,8 7,12 13,4" /></svg>
<span class="ts06-num">1</span>
</span>
<span class="ts06-label">カート</span>
</div>
<span class="ts06-bar"><span class="ts06-fill"></span></span>
<div class="ts06-step">
<span class="ts06-circle" aria-label="完了">
<svg class="ts06-check" viewBox="0 0 16 16" fill="none" aria-hidden="true"><polyline points="3,8 7,12 13,4" /></svg>
<span class="ts06-num">2</span>
</span>
<span class="ts06-label">配送</span>
</div>
<span class="ts06-bar"><span class="ts06-fill"></span></span>
<div class="ts06-step ts06-step--active">
<span class="ts06-circle"><span class="ts06-num">3</span></span>
<span class="ts06-label">決済</span>
</div>
<span class="ts06-bar"></span>
<div class="ts06-step">
<span class="ts06-circle"><span class="ts06-num">4</span></span>
<span class="ts06-label">完了</span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop;
$easing: $ease-out;
$color: $stage-ink;
@include stage($stage-yellow);
.ts06-steps {
display: flex;
align-items: flex-start;
width: min(340px, 90vw);
}
.ts06-step {
flex-grow: 0;
flex-shrink: 0;
flex-basis: auto;
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-2;
color: $color;
font: 600 11px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.ts06-circle {
position: relative;
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(23, 20, 26, .12);
display: grid;
place-items: center;
}
.ts06-num { font-weight: 700; font-size: 13px; line-height: 1; font-family: inherit; }
// 체크 아이콘: SVG polyline 을 stroke-dashoffset 으로 그린다 (BD16: 막대 두 개 사선 대신 안전한 방법)
.ts06-check {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
padding: 8px;
box-sizing: border-box;
opacity: 0;
polyline {
fill: none;
stroke: $subject-cream;
stroke-width: 2.4;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 16;
stroke-dashoffset: 16;
}
}
.ts06-num { opacity: 0; }
// 실제 사용 코드: 끝난 단계는 JS 가 .is-checked 를 붙이면 체크가 그려지고 숫자는 사라진다
.ts06-step.is-checked .ts06-circle { background: $color; }
.ts06-step.is-checked .ts06-check { opacity: 1; }
.ts06-step.is-checked .ts06-check polyline { animation: ts06-draw .5s $easing forwards; }
.ts06-step.is-checked .ts06-num { display: none; }
.ts06-step:not(.is-checked) .ts06-num { opacity: 1; }
.ts06-step--active .ts06-circle { background: $color; color: $subject-cream; }
.ts06-step--active .ts06-num { opacity: 1; color: $subject-cream; }
.ts06-step.is-checked + .ts06-bar .ts06-fill { transform: scaleX(1); }
.ts06-bar {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
height: 3px;
margin-top: 16px;
background: rgba(23, 20, 26, .14);
border-radius: $r-xs;
overflow: hidden;
display: block;
}
.ts06-fill {
display: block;
width: 100%;
height: 100%;
background: $color;
transform: scaleX(0);
transform-origin: left; // 규칙8: 체크아웃 연결선 좌측 채움
}
@keyframes ts06-draw { from { stroke-dashoffset: 16; } to { stroke-dashoffset: 0; } }
// 데모 루프: 1·2 는 이미 완료, 3(결제)이 활성 상태로 펄스
.ts06-steps.is-demo .ts06-step--active .ts06-circle { animation: ts06-active-pulse 1.2s $easing infinite; }
.ts06-steps.is-demo .ts06-step:nth-child(1) .ts06-circle,
.ts06-steps.is-demo .ts06-step:nth-child(3) .ts06-circle { background: $color; }
.ts06-steps.is-demo .ts06-step:nth-child(1) .ts06-check,
.ts06-steps.is-demo .ts06-step:nth-child(3) .ts06-check { opacity: 1; }
.ts06-steps.is-demo .ts06-step:nth-child(1) .ts06-num,
.ts06-steps.is-demo .ts06-step:nth-child(3) .ts06-num { display: none; }
.ts06-steps.is-demo .ts06-step:nth-child(1) .ts06-check polyline { animation: ts06-draw-loop $duration $easing infinite; }
.ts06-steps.is-demo .ts06-step:nth-child(3) .ts06-check polyline { animation: ts06-draw-loop $duration $easing infinite; animation-delay: .35s; }
.ts06-steps.is-demo .ts06-bar:nth-child(2) .ts06-fill { animation: ts06-fill-loop $duration $easing infinite; }
.ts06-steps.is-demo .ts06-bar:nth-child(4) .ts06-fill { animation: ts06-fill-loop $duration $easing infinite; animation-delay: .35s; }
@keyframes ts06-draw-loop {
0%, 5% { stroke-dashoffset: 16; }
35%, 100% { stroke-dashoffset: 0; }
}
@keyframes ts06-fill-loop {
0%, 8% { transform: scaleX(0); }
40%, 100% { transform: scaleX(1); }
}
@keyframes ts06-active-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(23, 20, 26, .35); }
50% { box-shadow: 0 0 0 6px rgba(23, 20, 26, 0); }
}
@media (prefers-reduced-motion: reduce) {
.ts06-check polyline, .ts06-steps.is-demo .ts06-check polyline { animation: none !important; stroke-dashoffset: 0 !important; }
.ts06-fill, .ts06-steps.is-demo .ts06-fill { animation: none !important; }
.ts06-steps.is-demo .ts06-step--active .ts06-circle { 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);
}
.ts06-steps {
display: flex;
align-items: flex-start;
width: min(340px, 90vw);
}
.ts06-step {
flex-grow: 0;
flex-shrink: 0;
flex-basis: auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
color: #17141a;
font: 600 11px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.ts06-circle {
position: relative;
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(23, 20, 26, 0.12);
display: grid;
place-items: center;
}
.ts06-num {
font-weight: 700;
font-size: 13px;
line-height: 1;
font-family: inherit;
}
.ts06-check {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
padding: 8px;
box-sizing: border-box;
opacity: 0;
}
.ts06-check polyline {
fill: none;
stroke: #fff7e6;
stroke-width: 2.4;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 16;
stroke-dashoffset: 16;
}
.ts06-num {
opacity: 0;
}
.ts06-step.is-checked .ts06-circle {
background: #17141a;
}
.ts06-step.is-checked .ts06-check {
opacity: 1;
}
.ts06-step.is-checked .ts06-check polyline {
animation: ts06-draw 0.5s ease-out forwards;
}
.ts06-step.is-checked .ts06-num {
display: none;
}
.ts06-step:not(.is-checked) .ts06-num {
opacity: 1;
}
.ts06-step--active .ts06-circle {
background: #17141a;
color: #fff7e6;
}
.ts06-step--active .ts06-num {
opacity: 1;
color: #fff7e6;
}
.ts06-step.is-checked + .ts06-bar .ts06-fill {
transform: scaleX(1);
}
.ts06-bar {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
height: 3px;
margin-top: 16px;
background: rgba(23, 20, 26, 0.14);
border-radius: 4px;
overflow: hidden;
display: block;
}
.ts06-fill {
display: block;
width: 100%;
height: 100%;
background: #17141a;
transform: scaleX(0);
transform-origin: left;
}
@keyframes ts06-draw {
from {
stroke-dashoffset: 16;
}
to {
stroke-dashoffset: 0;
}
}
.ts06-steps.is-demo .ts06-step--active .ts06-circle {
animation: ts06-active-pulse 1.2s ease-out infinite;
}
.ts06-steps.is-demo .ts06-step:nth-child(1) .ts06-circle,
.ts06-steps.is-demo .ts06-step:nth-child(3) .ts06-circle {
background: #17141a;
}
.ts06-steps.is-demo .ts06-step:nth-child(1) .ts06-check,
.ts06-steps.is-demo .ts06-step:nth-child(3) .ts06-check {
opacity: 1;
}
.ts06-steps.is-demo .ts06-step:nth-child(1) .ts06-num,
.ts06-steps.is-demo .ts06-step:nth-child(3) .ts06-num {
display: none;
}
.ts06-steps.is-demo .ts06-step:nth-child(1) .ts06-check polyline {
animation: ts06-draw-loop 2s ease-out infinite;
}
.ts06-steps.is-demo .ts06-step:nth-child(3) .ts06-check polyline {
animation: ts06-draw-loop 2s ease-out infinite;
animation-delay: 0.35s;
}
.ts06-steps.is-demo .ts06-bar:nth-child(2) .ts06-fill {
animation: ts06-fill-loop 2s ease-out infinite;
}
.ts06-steps.is-demo .ts06-bar:nth-child(4) .ts06-fill {
animation: ts06-fill-loop 2s ease-out infinite;
animation-delay: 0.35s;
}
@keyframes ts06-draw-loop {
0%, 5% {
stroke-dashoffset: 16;
}
35%, 100% {
stroke-dashoffset: 0;
}
}
@keyframes ts06-fill-loop {
0%, 8% {
transform: scaleX(0);
}
40%, 100% {
transform: scaleX(1);
}
}
@keyframes ts06-active-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(23, 20, 26, 0.35);
}
50% {
box-shadow: 0 0 0 6px rgba(23, 20, 26, 0);
}
}
@media (prefers-reduced-motion: reduce) {
.ts06-check polyline, .ts06-steps.is-demo .ts06-check polyline {
animation: none !important;
stroke-dashoffset: 0 !important;
}
.ts06-fill, .ts06-steps.is-demo .ts06-fill {
animation: none !important;
}
.ts06-steps.is-demo .ts06-step--active .ts06-circle {
animation: none !important;
}
}import { CSSProperties } from "react";
import styles from "./CheckoutStepper.module.scss";
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
export interface CheckoutStepperProps {
duration?: string;
easing?: string;
color?: string;
labels?: string[];
activeIndex?: number;
}
export default function CheckoutStepper({
duration = "500ms",
easing = "ease-out",
color = "#17141a",
labels = ["장바구니", "배송", "결제", "완료"],
activeIndex = 2,
}: CheckoutStepperProps) {
const style: Vars = { "--duration": duration, "--easing": easing, "--color": color };
return (
<div className={styles.steps} style={style}>
{labels.map((label, i) => (
<div key={label} style={{ display: "contents" }}>
<div className={styles.step}>
<span className={`${styles.circle} ${i < activeIndex ? styles.checked : ""} ${i === activeIndex ? styles.active : ""}`} aria-label={i < activeIndex ? "완료" : undefined}>
{i < activeIndex ? (
<svg className={styles.check} viewBox="0 0 16 16" fill="none" aria-hidden="true">
<polyline points="3,8 7,12 13,4" />
</svg>
) : (
<span className={styles.num}>{i + 1}</span>
)}
</span>
<span>{label}</span>
</div>
{i < labels.length - 1 && (
<span className={styles.bar}>
<span className={`${styles.fill} ${i < activeIndex - 1 ? styles.filled : ""}`} />
</span>
)}
</div>
))}
</div>
);
}06 チェックアウトステッパー — 計測 累積面積 0.77% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · 【コピペOK】タイムライン デザイン css 9選
このデモが属する記事
【コピペOK】タイムライン デザイン css 9選会社の沿革ページや通販サイトの注文状況のように、いくつかの段階を線と丸でつないで順番に見せる部品を「タイムライン」と呼びます。タイムライン デザイン cssで検索して出てくるような、線が伸びたり丸が光ったりする動きを、AOS・GSAPなしの この記事のデモをすべて見る


