02スパークライン表示の統計カード
카드에 마우스를 올리면 그 자리에 숨어 있던 미니 그래프 선이 실제로 그려지고, 위 기간 스위치를 누르면 세 카드 숫자가 한 번에 교체됩니다.
使いどころ
stroke-dashoffsetopacitymouseenter

<main class="stage">
<div class="sc" role="group" aria-label="主要指標">
<div class="sc__head">
<span class="sc__title">今週</span>
<div class="sc__switch" role="group" aria-label="期間">
<button class="sc__period is-active" type="button" data-period="week">週間</button>
<button class="sc__period" type="button" data-period="month">月間</button>
</div>
</div>
<div class="sc__row">
<div class="sc__card is-demo">
<span class="sc__label">訪問者</span>
<span class="sc__value" data-week="4,210" data-month="16,940">4,210</span>
<svg class="sc__spark" viewBox="0 0 60 24" aria-hidden="true">
<path class="sc__spark-path" pathLength="100" d="M2 18 L12 14 L22 16 L32 8 L42 10 L50 4 L58 6" />
</svg>
</div>
<div class="sc__card is-demo">
<span class="sc__label">コンバージョン率</span>
<span class="sc__value" data-week="3.2%" data-month="3.6%">3.2%</span>
<svg class="sc__spark" viewBox="0 0 60 24" aria-hidden="true">
<path class="sc__spark-path" pathLength="100" d="M2 8 L12 10 L22 6 L32 12 L42 9 L50 14 L58 11" />
</svg>
</div>
<div class="sc__card is-demo">
<span class="sc__label">売上</span>
<span class="sc__value" data-week="84.2万円" data-month="₩3,180만">84.2万円</span>
<svg class="sc__spark" viewBox="0 0 60 24" aria-hidden="true">
<path class="sc__spark-path" pathLength="100" d="M2 20 L12 16 L22 17 L32 11 L42 7 L50 9 L58 3" />
</svg>
</div>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-paper);
.sc {
box-sizing: border-box;
width: min(420px, 92vw);
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.sc__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: $sp-3; }
.sc__title { font-weight: 800; font-size: 13px; color: $stage-ink; }
.sc__switch { display: flex; gap: $sp-1; padding: $sp-1; border-radius: $r-pill; background: rgba(23, 20, 26, .06); }
.sc__period {
border: 0; background: transparent; cursor: pointer;
padding: $sp-1 $sp-3; border-radius: $r-pill;
font-weight: 700; font-size: 10px; font-family: inherit; color: rgba(23, 20, 26, .72);
transition: background $duration $easing, color $duration $easing;
}
.sc__period.is-active { background: $stage-ink; color: $subject-cream; }
.sc__row { display: flex; gap: $sp-3; }
.sc__card {
box-sizing: border-box;
flex: 1 1 0;
min-width: 0;
border-radius: $r-card;
padding: $sp-3;
background: #fff;
box-shadow: $shadow-raised;
position: relative;
overflow: hidden;
}
.sc__label { display: block; font-size: 10px; font-weight: 700; color: rgba(23, 20, 26, .72); margin-bottom: $sp-1; }
.sc__value {
display: block; font-size: clamp(14px, 4vw, 18px); font-weight: 800; color: $stage-ink;
transition: opacity $dur-quick $easing;
}
.sc__value.is-swap { opacity: 0; }
.sc__spark { display: block; width: 100%; height: 24px; margin-top: $sp-2; overflow: visible; }
.sc__spark-path {
fill: none;
stroke: $color;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 100;
stroke-dashoffset: 100;
}
// ── 자동 루프: 스파크라인이 계속 그려진다 (호버 없이도 보이도록) ──────────────
.sc__card.is-demo .sc__spark-path { animation: sc-draw 2s $easing infinite; }
@keyframes sc-draw {
0% { stroke-dashoffset: 100; }
60% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: 0; }
}
@media (max-width: 340px) {
.sc__row { gap: $sp-2; }
.sc__card { padding: $sp-2; }
.sc__label { font-size: 9px; }
}
@media (prefers-reduced-motion: reduce) {
.sc__card.is-demo .sc__spark-path { animation: none !important; stroke-dashoffset: 0; }
.sc__value { transition: none; }
}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);
}
.sc {
box-sizing: border-box;
width: min(420px, 92vw);
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.sc__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.sc__title {
font-weight: 800;
font-size: 13px;
color: #17141a;
}
.sc__switch {
display: flex;
gap: 4px;
padding: 4px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.06);
}
.sc__period {
border: 0;
background: transparent;
cursor: pointer;
padding: 4px 12px;
border-radius: 999px;
font-weight: 700;
font-size: 10px;
font-family: inherit;
color: rgba(23, 20, 26, 0.72);
transition: background 300ms ease-out, color 300ms ease-out;
}
.sc__period.is-active {
background: #17141a;
color: #fff7e6;
}
.sc__row {
display: flex;
gap: 12px;
}
.sc__card {
box-sizing: border-box;
flex: 1 1 0;
min-width: 0;
border-radius: 16px;
padding: 12px;
background: #fff;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
position: relative;
overflow: hidden;
}
.sc__label {
display: block;
font-size: 10px;
font-weight: 700;
color: rgba(23, 20, 26, 0.72);
margin-bottom: 4px;
}
.sc__value {
display: block;
font-size: clamp(14px, 4vw, 18px);
font-weight: 800;
color: #17141a;
transition: opacity 220ms ease-out;
}
.sc__value.is-swap {
opacity: 0;
}
.sc__spark {
display: block;
width: 100%;
height: 24px;
margin-top: 8px;
overflow: visible;
}
.sc__spark-path {
fill: none;
stroke: #2f6df6;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 100;
stroke-dashoffset: 100;
}
.sc__card.is-demo .sc__spark-path {
animation: sc-draw 2s ease-out infinite;
}
@keyframes sc-draw {
0% {
stroke-dashoffset: 100;
}
60% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: 0;
}
}
@media (max-width: 340px) {
.sc__row {
gap: 8px;
}
.sc__card {
padding: 8px;
}
.sc__label {
font-size: 9px;
}
}
@media (prefers-reduced-motion: reduce) {
.sc__card.is-demo .sc__spark-path {
animation: none !important;
stroke-dashoffset: 0;
}
.sc__value {
transition: none;
}
}// 실제 동작: 週間/月間 버튼을 누르면 세 카드 숫자가 실제로 교체된다(크로스페이드). 첫 클릭에 자동 루프를 멈춘다.
var buttons = document.querySelectorAll('.sc__period');
var cards = document.querySelectorAll('.sc__card');
buttons.forEach(function (b) {
b.addEventListener('click', function () {
cards.forEach(function (c) { c.classList.remove('is-demo'); });
buttons.forEach(function (o) { o.classList.remove('is-active'); });
b.classList.add('is-active');
var period = b.dataset.period;
cards.forEach(function (c) {
var v = c.querySelector('.sc__value');
v.classList.add('is-swap');
setTimeout(function () {
v.textContent = v.dataset[period];
v.classList.remove('is-swap');
}, 140);
});
});
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./StatCards.module.scss";
export type StatCardsProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type Period = "week" | "month";
const CARDS: { label: string; week: string; month: string; path: string }[] = [
{ label: "방문자", week: "4,210", month: "16,940", path: "M2 18 L12 14 L22 16 L32 8 L42 10 L50 4 L58 6" },
{ label: "전환율", week: "3.2%", month: "3.6%", path: "M2 8 L12 10 L22 6 L32 12 L42 9 L50 14 L58 11" },
{ label: "매출", week: "₩842만", month: "₩3,180만", path: "M2 20 L12 16 L22 17 L32 11 L42 7 L50 9 L58 3" },
];
export default function StatCards({ duration = 220, easing = "ease-out", color = "#2f6df6" }: StatCardsProps) {
const [period, setPeriod] = useState<Period>("week");
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.sc} style={vars} role="group" aria-label="핵심 지표">
<div className={styles.head}>
<span className={styles.title}>이번 주</span>
<div className={styles.switch} role="group" aria-label="기간">
{(["week", "month"] as Period[]).map((p) => (
<button
key={p}
type="button"
className={period === p ? styles.active : undefined}
onClick={() => setPeriod(p)}
>
{p === "week" ? "주간" : "월간"}
</button>
))}
</div>
</div>
<div className={styles.row}>
{CARDS.map((c) => (
<div className={styles.card} key={c.label}>
<span className={styles.label}>{c.label}</span>
<span className={styles.value}>{period === "week" ? c.week : c.month}</span>
<svg className={styles.spark} viewBox="0 0 60 24" aria-hidden="true">
<path pathLength={100} d={c.path} />
</svg>
</div>
))}
</div>
</div>
);
}02 スパークライン表示の統計カード — 計測 累積面積 0.374% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · ダッシュボード ウィジェット単体UI9選 —そのまま貼れる部品だけ
このデモが属する記事
ダッシュボード ウィジェット単体UI9選 —そのまま貼れる部品だけ管理画面をまるごと使うのではなく、この部品だけを自分のプロジェクトにそのまま貼りたい人のために、単体で動くダッシュボード ウィジェットを9個集めました。ドーナツチャートは実際に数字が数え上がります。 この記事のデモをすべて見る

