UI Components Easy
Sparkline
A compact inline SVG sparkline chart for embedding in tables, cards, or dashboards. Supports line and bar variants, trend coloring (up/green, down/red), and an optional last-value dot.
Open in Lab
MCP
svg react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #0f1117;
--surface: #16181f;
--surface2: #1e2130;
--border: #2a2d3a;
--text: #e2e8f0;
--text-muted: #64748b;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 40px 24px;
}
.page {
max-width: 700px;
margin: 0 auto;
}
.page-title {
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 24px;
}
.sparkline-table {
display: flex;
flex-direction: column;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
}
.sl-row {
display: grid;
grid-template-columns: 140px 1fr 80px 60px;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
gap: 12px;
font-size: 0.875rem;
}
.sl-row:last-child {
border-bottom: none;
}
.sl-row--header {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--text-muted);
background: var(--surface2);
}
.sl-cell {
display: flex;
align-items: center;
}
.sl-current {
font-weight: 700;
text-align: right;
}
.sl-trend {
font-size: 0.75rem;
font-weight: 700;
text-align: right;
}
.sl-trend.up {
color: #34d399;
}
.sl-trend.down {
color: #f87171;
}const W = 120,
H = 36;
document.querySelectorAll(".sl-row[data-values]").forEach((row) => {
const values = row.dataset.values.split(",").map(Number);
const color = row.dataset.color;
// Build SVG sparkline
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", W);
svg.setAttribute("height", H);
svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
const min = Math.min(...values),
max = Math.max(...values);
const xOf = (i) => (i / (values.length - 1)) * W;
const yOf = (v) => H - 2 - ((v - min) / (max - min || 1)) * (H - 6);
// Gradient area
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
defs.innerHTML = `<linearGradient id="sg_${color.replace("#", "")}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${color}" stop-opacity="0.35"/>
<stop offset="100%" stop-color="${color}" stop-opacity="0"/>
</linearGradient>`;
svg.appendChild(defs);
const pts = values.map((v, i) => [xOf(i), yOf(v)]);
const areaD = `M${pts[0][0]},${H} ${pts.map((p) => p.join(",")).join(" ")} L${pts[pts.length - 1][0]},${H} Z`;
const area = document.createElementNS("http://www.w3.org/2000/svg", "path");
area.setAttribute("d", areaD);
area.setAttribute("fill", `url(#sg_${color.replace("#", "")})`);
svg.appendChild(area);
const line = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
line.setAttribute("points", pts.map((p) => p.join(",")).join(" "));
line.setAttribute("fill", "none");
line.setAttribute("stroke", color);
line.setAttribute("stroke-width", "2");
line.setAttribute("stroke-linecap", "round");
svg.appendChild(line);
// Last dot
const last = pts[pts.length - 1];
const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
dot.setAttribute("cx", last[0]);
dot.setAttribute("cy", last[1]);
dot.setAttribute("r", "3");
dot.setAttribute("fill", color);
dot.setAttribute("stroke", "#0f1117");
dot.setAttribute("stroke-width", "1.5");
svg.appendChild(dot);
row.querySelector(".sl-cell").appendChild(svg);
// Current value
const cur = values[values.length - 1];
row.querySelector(".sl-current").textContent = cur % 1 === 0 ? cur : cur.toFixed(1);
// Trend
const prev = values[values.length - 2];
const delta = (((cur - prev) / prev) * 100).toFixed(1);
const tEl = row.querySelector(".sl-trend");
tEl.textContent = (delta > 0 ? "▲" : delta < 0 ? "▼" : "—") + Math.abs(delta) + "%";
tEl.classList.add(delta > 0 ? "up" : "down");
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Sparklines</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="page">
<h1 class="page-title">Sparklines</h1>
<div class="sparkline-table">
<div class="sl-row sl-row--header">
<span>Metric</span><span>Last 12 weeks</span><span>Current</span><span>Trend</span>
</div>
<div class="sl-row" data-values="42,38,45,51,48,55,60,58,62,70,68,75" data-color="#818cf8">
<span>Revenue</span><span class="sl-cell"></span><span class="sl-current"></span><span
class="sl-trend"></span>
</div>
<div class="sl-row" data-values="120,115,130,125,140,135,145,150,148,160,155,170" data-color="#34d399">
<span>Users</span><span class="sl-cell"></span><span class="sl-current"></span><span
class="sl-trend"></span>
</div>
<div class="sl-row" data-values="3.2,3.8,3.5,3.9,3.7,3.4,4.1,3.8,4.2,3.9,4.0,3.6" data-color="#f59e0b">
<span>Avg Session</span><span class="sl-cell"></span><span class="sl-current"></span><span
class="sl-trend"></span>
</div>
<div class="sl-row" data-values="12,18,15,22,19,25,21,27,24,30,28,35" data-color="#f87171">
<span>Errors</span><span class="sl-cell"></span><span class="sl-current"></span><span
class="sl-trend"></span>
</div>
</div>
</main>
<script src="script.js"></script>
</body>
</html>const W = 120,
H = 36;
const ROWS = [
{
label: "Revenue",
values: [3200, 2900, 3800, 4100, 3700, 4500, 4900, 4200, 5100, 4700, 5300, 5800],
color: "#818cf8",
suffix: "k",
},
{
label: "Active Users",
values: [810, 770, 840, 920, 880, 960, 1010, 980, 1050, 990, 1080, 1120],
color: "#34d399",
suffix: "",
},
{
label: "Conversion",
values: [2.1, 2.4, 2.2, 2.8, 2.6, 3.1, 2.9, 3.3, 3.0, 3.5, 3.2, 3.8],
color: "#f59e0b",
suffix: "%",
},
{
label: "Bounce Rate",
values: [54, 58, 52, 49, 55, 47, 50, 46, 48, 45, 43, 41],
color: "#f87171",
suffix: "%",
},
{
label: "Avg. Session",
values: [2.1, 2.3, 2.0, 2.4, 2.2, 2.6, 2.5, 2.8, 2.7, 3.0, 2.9, 3.2],
color: "#a78bfa",
suffix: "m",
},
];
function Sparkline({ values, color }: { values: number[]; color: string }) {
const min = Math.min(...values),
max = Math.max(...values);
const xOf = (i: number) => (i / (values.length - 1)) * W;
const yOf = (v: number) => H - 2 - ((v - min) / (max - min || 1)) * (H - 6);
const pts = values.map((v, i): [number, number] => [xOf(i), yOf(v)]);
const ptsStr = pts.map((p) => p.join(",")).join(" ");
const areaD = `M${pts[0][0]},${H} ${ptsStr} L${pts[pts.length - 1][0]},${H} Z`;
const id = `sg${color.replace("#", "")}`;
const last = pts[pts.length - 1];
return (
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}>
<defs>
<linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity={0.35} />
<stop offset="100%" stopColor={color} stopOpacity={0} />
</linearGradient>
</defs>
<path d={areaD} fill={`url(#${id})`} />
<polyline
points={ptsStr}
fill="none"
stroke={color}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle cx={last[0]} cy={last[1]} r={3} fill={color} stroke="#0f1117" strokeWidth={1.5} />
</svg>
);
}
export default function SparklineRC() {
return (
<div className="min-h-screen bg-[#0d1117] p-6 flex justify-center">
<div className="w-full max-w-[600px] bg-[#0d1117] rounded-xl border border-[#21262d] overflow-hidden">
<div className="grid grid-cols-[1fr_auto_auto_auto] px-4 py-2 border-b border-[#21262d] text-[11px] text-[#484f58] font-semibold uppercase tracking-wider">
<span>Metric</span>
<span className="text-right pr-6">Trend</span>
<span className="text-right pr-4">Current</span>
<span className="text-right">Change</span>
</div>
{ROWS.map((row) => {
const cur = row.values[row.values.length - 1];
const prev = row.values[row.values.length - 2];
const delta = (((cur - prev) / prev) * 100).toFixed(1);
const isUp = +delta > 0;
const fmt = (v: number) => (v % 1 === 0 ? v : v.toFixed(1)) + row.suffix;
return (
<div
key={row.label}
className="grid grid-cols-[1fr_auto_auto_auto] items-center px-4 py-3 border-b border-[#21262d] last:border-0 hover:bg-[#161b22] transition-colors"
>
<div className="flex items-center gap-2">
<span
className="w-2 h-2 rounded-full flex-shrink-0"
style={{ background: row.color }}
/>
<span className="text-[#8b949e] text-[13px]">{row.label}</span>
</div>
<div className="pr-6">
<Sparkline values={row.values} color={row.color} />
</div>
<div className="text-[#e6edf3] font-semibold text-[14px] pr-4">{fmt(cur)}</div>
<div
className={`text-[12px] font-semibold text-right ${isUp ? "text-[#34d399]" : "text-[#f87171]"}`}
>
{isUp ? "▲" : "▼"}
{Math.abs(+delta)}%
</div>
</div>
);
})}
</div>
</div>
);
}<script setup>
const W = 120,
H = 36;
const ROWS = [
{
label: "Revenue",
values: [3200, 2900, 3800, 4100, 3700, 4500, 4900, 4200, 5100, 4700, 5300, 5800],
color: "#818cf8",
suffix: "k",
},
{
label: "Active Users",
values: [810, 770, 840, 920, 880, 960, 1010, 980, 1050, 990, 1080, 1120],
color: "#34d399",
suffix: "",
},
{
label: "Conversion",
values: [2.1, 2.4, 2.2, 2.8, 2.6, 3.1, 2.9, 3.3, 3.0, 3.5, 3.2, 3.8],
color: "#f59e0b",
suffix: "%",
},
{
label: "Bounce Rate",
values: [54, 58, 52, 49, 55, 47, 50, 46, 48, 45, 43, 41],
color: "#f87171",
suffix: "%",
},
{
label: "Avg. Session",
values: [2.1, 2.3, 2.0, 2.4, 2.2, 2.6, 2.5, 2.8, 2.7, 3.0, 2.9, 3.2],
color: "#a78bfa",
suffix: "m",
},
];
function sparklineData(values, color) {
const min = Math.min(...values),
max = Math.max(...values);
const xOf = (i) => (i / (values.length - 1)) * W;
const yOf = (v) => H - 2 - ((v - min) / (max - min || 1)) * (H - 6);
const pts = values.map((v, i) => [xOf(i), yOf(v)]);
const ptsStr = pts.map((p) => p.join(",")).join(" ");
const areaD = `M${pts[0][0]},${H} ${ptsStr} L${pts[pts.length - 1][0]},${H} Z`;
const id = `sg${color.replace("#", "")}`;
const last = pts[pts.length - 1];
return { ptsStr, areaD, id, last };
}
function fmt(v, suffix) {
return (v % 1 === 0 ? v : v.toFixed(1)) + suffix;
}
function rowMeta(row) {
const cur = row.values[row.values.length - 1];
const prev = row.values[row.values.length - 2];
const delta = (((cur - prev) / prev) * 100).toFixed(1);
const isUp = +delta > 0;
return { cur, delta, isUp };
}
</script>
<template>
<div class="wrapper">
<div class="table">
<div class="header">
<span>Metric</span>
<span class="right pr6">Trend</span>
<span class="right pr4">Current</span>
<span class="right">Change</span>
</div>
<div
v-for="row in ROWS"
:key="row.label"
class="row"
>
<div class="metric">
<span class="dot" :style="{ background: row.color }"></span>
<span class="label">{{ row.label }}</span>
</div>
<div class="pr6">
<svg :width="W" :height="H" :viewBox="`0 0 ${W} ${H}`">
<defs>
<linearGradient :id="sparklineData(row.values, row.color).id" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" :stop-color="row.color" stop-opacity="0.35" />
<stop offset="100%" :stop-color="row.color" stop-opacity="0" />
</linearGradient>
</defs>
<path :d="sparklineData(row.values, row.color).areaD" :fill="`url(#${sparklineData(row.values, row.color).id})`" />
<polyline :points="sparklineData(row.values, row.color).ptsStr" fill="none" :stroke="row.color" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle
:cx="sparklineData(row.values, row.color).last[0]"
:cy="sparklineData(row.values, row.color).last[1]"
r="3"
:fill="row.color"
stroke="#0f1117"
stroke-width="1.5"
/>
</svg>
</div>
<div class="current">{{ fmt(rowMeta(row).cur, row.suffix) }}</div>
<div class="change" :class="rowMeta(row).isUp ? 'up' : 'down'">
{{ rowMeta(row).isUp ? '\u25B2' : '\u25BC' }}{{ Math.abs(+rowMeta(row).delta) }}%
</div>
</div>
</div>
</div>
</template>
<style scoped>
.wrapper {
min-height: 100vh;
background: #0d1117;
padding: 1.5rem;
display: flex;
justify-content: center;
font-family: system-ui, -apple-system, sans-serif;
}
.table {
width: 100%;
max-width: 600px;
background: #0d1117;
border-radius: 0.75rem;
border: 1px solid #21262d;
overflow: hidden;
}
.header {
display: grid;
grid-template-columns: 1fr auto auto auto;
padding: 0.5rem 1rem;
border-bottom: 1px solid #21262d;
font-size: 11px;
color: #484f58;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.right { text-align: right; }
.pr6 { padding-right: 1.5rem; }
.pr4 { padding-right: 1rem; }
.row {
display: grid;
grid-template-columns: 1fr auto auto auto;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid #21262d;
transition: background 0.15s;
}
.row:last-child { border-bottom: 0; }
.row:hover { background: #161b22; }
.metric { display: flex; align-items: center; gap: 0.5rem; }
.dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; flex-shrink: 0; }
.label { color: #8b949e; font-size: 13px; }
.current { color: #e6edf3; font-weight: 600; font-size: 14px; padding-right: 1rem; }
.change { font-size: 12px; font-weight: 600; text-align: right; }
.up { color: #34d399; }
.down { color: #f87171; }
</style><script>
const W = 120,
H = 36;
const ROWS = [
{
label: "Revenue",
values: [3200, 2900, 3800, 4100, 3700, 4500, 4900, 4200, 5100, 4700, 5300, 5800],
color: "#818cf8",
suffix: "k",
},
{
label: "Active Users",
values: [810, 770, 840, 920, 880, 960, 1010, 980, 1050, 990, 1080, 1120],
color: "#34d399",
suffix: "",
},
{
label: "Conversion",
values: [2.1, 2.4, 2.2, 2.8, 2.6, 3.1, 2.9, 3.3, 3.0, 3.5, 3.2, 3.8],
color: "#f59e0b",
suffix: "%",
},
{
label: "Bounce Rate",
values: [54, 58, 52, 49, 55, 47, 50, 46, 48, 45, 43, 41],
color: "#f87171",
suffix: "%",
},
{
label: "Avg. Session",
values: [2.1, 2.3, 2.0, 2.4, 2.2, 2.6, 2.5, 2.8, 2.7, 3.0, 2.9, 3.2],
color: "#a78bfa",
suffix: "m",
},
];
function sparklineData(values, color) {
const min = Math.min(...values),
max = Math.max(...values);
const xOf = (i) => (i / (values.length - 1)) * W;
const yOf = (v) => H - 2 - ((v - min) / (max - min || 1)) * (H - 6);
const pts = values.map((v, i) => [xOf(i), yOf(v)]);
const ptsStr = pts.map((p) => p.join(",")).join(" ");
const areaD = `M${pts[0][0]},${H} ${ptsStr} L${pts[pts.length - 1][0]},${H} Z`;
const id = `sg${color.replace("#", "")}`;
const last = pts[pts.length - 1];
return { ptsStr, areaD, id, last };
}
function fmt(v, suffix) {
return (v % 1 === 0 ? v : v.toFixed(1)) + suffix;
}
function rowData(row) {
const cur = row.values[row.values.length - 1];
const prev = row.values[row.values.length - 2];
const delta = (((cur - prev) / prev) * 100).toFixed(1);
const isUp = +delta > 0;
return { cur, delta, isUp };
}
</script>
<div class="wrapper">
<div class="table">
<div class="header">
<span>Metric</span>
<span class="right pr6">Trend</span>
<span class="right pr4">Current</span>
<span class="right">Change</span>
</div>
{#each ROWS as row}
{@const sl = sparklineData(row.values, row.color)}
{@const rd = rowData(row)}
<div class="row">
<div class="metric">
<span class="dot" style="background: {row.color};"></span>
<span class="label">{row.label}</span>
</div>
<div class="pr6">
<svg width={W} height={H} viewBox="0 0 {W} {H}">
<defs>
<linearGradient id={sl.id} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color={row.color} stop-opacity="0.35" />
<stop offset="100%" stop-color={row.color} stop-opacity="0" />
</linearGradient>
</defs>
<path d={sl.areaD} fill="url(#{sl.id})" />
<polyline points={sl.ptsStr} fill="none" stroke={row.color} stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx={sl.last[0]} cy={sl.last[1]} r="3" fill={row.color} stroke="#0f1117" stroke-width="1.5" />
</svg>
</div>
<div class="current">{fmt(rd.cur, row.suffix)}</div>
<div class="change" class:up={rd.isUp} class:down={!rd.isUp}>
{rd.isUp ? '▲' : '▼'}{Math.abs(+rd.delta)}%
</div>
</div>
{/each}
</div>
</div>
<style>
.wrapper {
min-height: 100vh;
background: #0d1117;
padding: 1.5rem;
display: flex;
justify-content: center;
font-family: system-ui, -apple-system, sans-serif;
}
.table {
width: 100%;
max-width: 600px;
background: #0d1117;
border-radius: 0.75rem;
border: 1px solid #21262d;
overflow: hidden;
}
.header {
display: grid;
grid-template-columns: 1fr auto auto auto;
padding: 0.5rem 1rem;
border-bottom: 1px solid #21262d;
font-size: 11px;
color: #484f58;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.right { text-align: right; }
.pr6 { padding-right: 1.5rem; }
.pr4 { padding-right: 1rem; }
.row {
display: grid;
grid-template-columns: 1fr auto auto auto;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid #21262d;
}
.row:last-child { border-bottom: 0; }
.row:hover { background: #161b22; }
.metric { display: flex; align-items: center; gap: 0.5rem; }
.dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; flex-shrink: 0; }
.label { color: #8b949e; font-size: 13px; }
.current { color: #e6edf3; font-weight: 600; font-size: 14px; padding-right: 1rem; }
.change { font-size: 12px; font-weight: 600; text-align: right; }
.up { color: #34d399; }
.down { color: #f87171; }
</style>Features
- Line & bar modes — switch between polyline and rect-bar rendering
- Trend color — automatically green if trending up, red if down
- Last-value dot — optional highlighted endpoint with tooltip
- Inline — fits inside table cells or card headers
- Zero baseline — bars always start from bottom; lines trace the data
How it works
- Min/max values normalize data points to the SVG viewport height
- A
<polyline>traces all normalized points for the line variant - Bar variant renders
<rect>elements with height proportional to value - The last point gets a
<circle>marker for the “current value” indicator