03Tô màu các bước ngang
Đường nối giữa các bước ngang được tô từ scaleX(0) lên scaleX(1) với transform-origin bên trái, nên những bước đã đi qua nối liền với nhau bằng màu.
Dùng choThanh tiến trình đăng ký, chỉ báo bước trong khảo sát
scaleXflex: 1 1 0600ms

<main class="stage">
<div class="ts03-steps is-demo">
<div class="ts03-step"><span class="ts03-circle">1</span><span class="ts03-label">Đăng ký</span></div>
<span class="ts03-bar"><span class="ts03-fill"></span></span>
<div class="ts03-step"><span class="ts03-circle">2</span><span class="ts03-label">Hồ sơ</span></div>
<span class="ts03-bar"><span class="ts03-fill"></span></span>
<div class="ts03-step"><span class="ts03-circle">3</span><span class="ts03-label">Hoàn tất</span></div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base;
$easing: $ease-out;
$color: $subject-ink; // 주황 무대 위 전경은 잉크 — 크림은 3.11:1
@include stage($stage-orange);
.ts03-steps {
display: flex;
align-items: flex-start;
width: min(320px, 88vw);
}
.ts03-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;
}
.ts03-circle {
width: 32px;
height: 32px;
border-radius: 50%;
display: grid;
place-items: center;
background: rgba(255, 255, 255, .18);
color: $color;
font-weight: 700;
font-size: 13px;
line-height: 1;
font-family: inherit;
}
// 연결선: 왼쪽부터 채워진다 (transform-origin: left, scaleX)
.ts03-bar {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
height: 3px;
margin-top: 16px;
background: rgba(255, 255, 255, .22);
border-radius: $r-xs;
overflow: hidden;
display: block;
}
.ts03-fill {
display: block;
width: 100%;
height: 100%;
background: $color;
transform: scaleX(0);
transform-origin: left; // 규칙8: 연결선 왼쪽부터 채움
}
// 실제 사용 코드: 단계가 끝나면 JS 가 .is-done 을 붙이고 채워진다
.ts03-step.is-done .ts03-circle { background: $subject-cream; color: $subject-orange-deep; } // 채워진 원 안 숫자 — 크림 면 위 deep 5.04:1
.ts03-step.is-done + .ts03-bar .ts03-fill { transform: scaleX(1); }
// 데모 루프: 1 → 2 → 3 순서로 채워지는 걸 2초에 한 번
.ts03-steps.is-demo .ts03-step:nth-child(1) .ts03-circle { animation: ts03-done $duration $easing infinite; }
.ts03-steps.is-demo .ts03-bar:nth-child(2) .ts03-fill { animation: ts03-fill $duration $easing infinite; animation-delay: .3s; }
.ts03-steps.is-demo .ts03-step:nth-child(3) .ts03-circle { animation: ts03-done $duration $easing infinite; animation-delay: .9s; }
.ts03-steps.is-demo .ts03-bar:nth-child(4) .ts03-fill { animation: ts03-fill $duration $easing infinite; animation-delay: 1.2s; }
@keyframes ts03-fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes ts03-done { from { background: rgba(255, 255, 255, .18); } to { background: $subject-cream; } }
@media (prefers-reduced-motion: reduce) {
.ts03-fill, .ts03-steps.is-demo .ts03-fill { animation: none !important; transform: scaleX(1) !important; }
.ts03-steps.is-demo .ts03-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: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ts03-steps {
display: flex;
align-items: flex-start;
width: min(320px, 88vw);
}
.ts03-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;
}
.ts03-circle {
width: 32px;
height: 32px;
border-radius: 50%;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.18);
color: #17141a;
font-weight: 700;
font-size: 13px;
line-height: 1;
font-family: inherit;
}
.ts03-bar {
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
height: 3px;
margin-top: 16px;
background: rgba(255, 255, 255, 0.22);
border-radius: 4px;
overflow: hidden;
display: block;
}
.ts03-fill {
display: block;
width: 100%;
height: 100%;
background: #17141a;
transform: scaleX(0);
transform-origin: left;
}
.ts03-step.is-done .ts03-circle {
background: #fff7e6;
color: #c93000;
}
.ts03-step.is-done + .ts03-bar .ts03-fill {
transform: scaleX(1);
}
.ts03-steps.is-demo .ts03-step:nth-child(1) .ts03-circle {
animation: ts03-done 300ms ease-out infinite;
}
.ts03-steps.is-demo .ts03-bar:nth-child(2) .ts03-fill {
animation: ts03-fill 300ms ease-out infinite;
animation-delay: 0.3s;
}
.ts03-steps.is-demo .ts03-step:nth-child(3) .ts03-circle {
animation: ts03-done 300ms ease-out infinite;
animation-delay: 0.9s;
}
.ts03-steps.is-demo .ts03-bar:nth-child(4) .ts03-fill {
animation: ts03-fill 300ms ease-out infinite;
animation-delay: 1.2s;
}
@keyframes ts03-fill {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
@keyframes ts03-done {
from {
background: rgba(255, 255, 255, 0.18);
}
to {
background: #fff7e6;
}
}
@media (prefers-reduced-motion: reduce) {
.ts03-fill, .ts03-steps.is-demo .ts03-fill {
animation: none !important;
transform: scaleX(1) !important;
}
.ts03-steps.is-demo .ts03-circle {
animation: none !important;
}
}import { CSSProperties } from "react";
import styles from "./HorizontalStepsFill.module.scss";
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
export interface HorizontalStepsFillProps {
duration?: string;
easing?: string;
color?: string;
labels?: string[];
doneCount?: number;
}
export default function HorizontalStepsFill({
duration = "600ms",
easing = "ease-out",
color = "#17141a",
labels = ["가입", "프로필", "완료"],
doneCount = 2,
}: HorizontalStepsFillProps) {
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 < doneCount ? styles.done : ""}`}>{i + 1}</span>
<span>{label}</span>
</div>
{i < labels.length - 1 && (
<span className={styles.bar}>
<span className={`${styles.fill} ${i < doneCount - 1 ? styles.filled : ""}`} />
</span>
)}
</div>
))}
</div>
);
}03 Tô màu các bước ngang — Diện tích đo được 1.597% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 css timeline animation không JS, copy-paste
Bài viết chứa demo này
9 css timeline animation không JS, copy-pasteCss timeline animation là cách nối nhiều bước bằng đường kẻ với vòng tròn để bày ra theo thứ tự. Chín mẫu dưới đây không Xem tất cả demo trong bài này


