Pages Hard
Data Dashboard
Startup metrics dashboard with scroll-linked number counters, gradient text hue-shift hero, animated Canvas 2D bar/donut/area charts, a milestone timeline, and team grid.
Open in Lab
MCP
gsap canvas-2d scrolltrigger lenis textplugin
Targets: JS HTML
Code
:root {
--bg: #070a12;
--text: #f0f4fb;
--panel: #121a2b;
--border: #263249;
--accent: #86e8ff;
--muted: #8a95a8;
--purple: #ae52ff;
--warm: #ffcc66;
--pink: #ff40d6;
--hue-shift: 0deg;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
overflow-x: hidden;
}
/* ── Hero ── */
.hero {
position: relative;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 5rem 2rem 3rem;
overflow: hidden;
}
.hero-bg {
position: absolute;
inset: 0;
background: radial-gradient(ellipse at 50% 30%, rgba(134, 232, 255, 0.08) 0%, transparent 65%),
radial-gradient(ellipse at 70% 70%, rgba(174, 82, 255, 0.06) 0%, transparent 60%);
pointer-events: none;
}
.hero-content {
position: relative;
max-width: 680px;
text-align: center;
z-index: 1;
}
.hero-badge {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--accent);
border: 1px solid rgba(134, 232, 255, 0.3);
border-radius: 20px;
padding: 0.3rem 1rem;
margin-bottom: 1.5rem;
}
.hero-title {
font-size: 4rem;
font-weight: 900;
line-height: 1.05;
margin-bottom: 1.5rem;
letter-spacing: -2px;
}
.hero-sub {
font-size: 1.15rem;
color: var(--muted);
max-width: 500px;
margin: 0 auto 2rem;
line-height: 1.8;
}
.hero-cta {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.btn-primary {
display: inline-block;
padding: 0.85rem 1.8rem;
background: var(--accent);
color: var(--bg);
border-radius: 6px;
font-weight: 700;
text-decoration: none;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: rgba(134, 232, 255, 0.85);
transform: translateY(-2px);
}
.btn-ghost {
display: inline-block;
padding: 0.85rem 1.8rem;
border: 1px solid var(--border);
color: var(--muted);
border-radius: 6px;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
}
.btn-ghost:hover {
border-color: var(--accent);
color: var(--accent);
transform: translateY(-2px);
}
.hero-scroll-hint {
position: absolute;
bottom: 2.5rem;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
color: var(--muted);
font-size: 0.7rem;
letter-spacing: 1px;
text-transform: uppercase;
}
.scroll-arrow {
width: 1px;
height: 30px;
background: linear-gradient(to bottom, var(--muted), transparent);
animation: scrollBounce 2s ease-in-out infinite;
}
@keyframes scrollBounce {
0%,
100% {
transform: scaleY(1);
opacity: 0.5;
}
50% {
transform: scaleY(1.3);
opacity: 1;
}
}
/* ── Gradient Text ── */
.gradient-text {
background: linear-gradient(
90deg,
hsl(var(--hue-shift), 80%, 55%),
hsl(calc(var(--hue-shift) + 60deg), 80%, 55%),
hsl(calc(var(--hue-shift) + 120deg), 80%, 55%)
);
background-size: 200% 100%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: gradShift 8s linear infinite;
}
@keyframes gradShift {
0% {
background-position: 0% center;
}
100% {
background-position: 200% center;
}
}
@supports not (-webkit-background-clip: text) {
.gradient-text {
color: var(--accent);
background: none;
}
}
/* ── Section Shared ── */
.section-header {
text-align: center;
max-width: 600px;
margin: 0 auto 4rem;
}
.section-tag {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 1rem;
}
.section-title {
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 1rem;
letter-spacing: -0.5px;
}
.section-desc {
font-size: 1rem;
color: var(--muted);
line-height: 1.8;
}
/* ── Metrics ── */
.metrics-section {
padding: 7rem 2rem;
border-top: 1px solid var(--border);
}
.metrics-grid {
max-width: 1100px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
.metric-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1.8rem 1.5rem;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.metric-card::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(134, 232, 255, 0.04) 0%, transparent 60%);
pointer-events: none;
}
.metric-card:hover {
border-color: var(--accent);
background: rgba(134, 232, 255, 0.06);
transform: translateY(-3px);
}
.metric-icon {
font-size: 1.2rem;
color: var(--accent);
margin-bottom: 1rem;
opacity: 0.7;
}
.metric-value {
font-size: 2.2rem;
font-weight: 800;
color: var(--accent);
letter-spacing: -1px;
margin-bottom: 0.4rem;
font-variant-numeric: tabular-nums;
}
.metric-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--text);
margin-bottom: 0.3rem;
}
.metric-sub {
font-size: 0.78rem;
color: var(--muted);
margin-bottom: 1rem;
}
.metric-bar {
height: 3px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
}
.metric-bar-fill {
height: 100%;
width: var(--bar-w, 50%);
background: linear-gradient(to right, var(--accent), var(--purple));
border-radius: 2px;
transform-origin: left center;
}
/* ── Charts ── */
.charts-section {
padding: 7rem 2rem;
border-top: 1px solid var(--border);
background: rgba(18, 26, 43, 0.5);
}
.charts-grid {
max-width: 1100px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
gap: 1.5rem;
}
.chart-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1.5rem;
transition: border-color 0.3s ease;
}
.chart-card:hover {
border-color: rgba(134, 232, 255, 0.3);
}
.chart-wide {
grid-column: 1 / -1;
}
.chart-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.2rem;
}
.chart-header h3 {
font-size: 0.95rem;
font-weight: 600;
color: var(--text);
}
.chart-badge {
font-size: 0.7rem;
font-weight: 700;
padding: 0.25rem 0.6rem;
border-radius: 20px;
background: rgba(134, 232, 255, 0.1);
color: var(--muted);
border: 1px solid var(--border);
}
.trend-up {
color: #4ade80;
background: rgba(74, 222, 128, 0.1);
border-color: rgba(74, 222, 128, 0.3);
}
canvas {
display: block;
width: 100%;
height: auto;
}
.chart-legend {
display: flex;
gap: 1.5rem;
margin-top: 1rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.8rem;
color: var(--muted);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.dot.accent {
background: rgba(134, 232, 255, 0.5);
}
.dot.purple {
background: var(--purple);
}
.donut-legend {
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dl-item {
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 0.82rem;
color: var(--muted);
}
.dl-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.dl-item span:nth-child(2) {
flex: 1;
}
.dl-item strong {
color: var(--text);
}
/* ── Timeline ── */
.timeline-section {
padding: 7rem 2rem;
border-top: 1px solid var(--border);
}
.timeline {
max-width: 700px;
margin: 0 auto;
position: relative;
}
.timeline::before {
content: "";
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 1px;
background: var(--border);
transform: translateX(-50%);
}
.tl-item {
display: flex;
align-items: flex-start;
margin-bottom: 3rem;
position: relative;
}
.tl-item[data-side="left"] {
flex-direction: row;
}
.tl-item[data-side="right"] {
flex-direction: row-reverse;
}
.tl-dot {
position: absolute;
left: 50%;
top: 1.2rem;
transform: translateX(-50%);
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--border);
border: 2px solid var(--panel);
z-index: 1;
}
.tl-dot--active {
background: var(--accent);
box-shadow: 0 0 12px rgba(134, 232, 255, 0.5);
}
.tl-card {
width: calc(50% - 2rem);
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.2rem 1.5rem;
}
.tl-card--active {
border-color: var(--accent);
background: rgba(134, 232, 255, 0.06);
}
.tl-item[data-side="left"] .tl-card {
margin-right: auto;
}
.tl-item[data-side="right"] .tl-card {
margin-left: auto;
}
.tl-date {
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--accent);
display: block;
margin-bottom: 0.4rem;
}
.tl-card h4 {
font-size: 1rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--text);
}
.tl-card p {
font-size: 0.85rem;
color: var(--muted);
line-height: 1.7;
}
/* ── Team ── */
.team-section {
padding: 7rem 2rem;
border-top: 1px solid var(--border);
background: rgba(18, 26, 43, 0.5);
}
.team-grid {
max-width: 800px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 1.5rem;
}
.team-card {
text-align: center;
padding: 1.5rem 1rem;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
transition: all 0.3s ease;
}
.team-card:hover {
border-color: var(--accent);
transform: translateY(-4px);
}
.team-avatar {
width: 56px;
height: 56px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
font-size: 0.85rem;
font-weight: 800;
letter-spacing: 1px;
background: hsl(var(--av-hue), 70%, 15%);
border: 2px solid hsl(var(--av-hue), 70%, 35%);
color: hsl(var(--av-hue), 70%, 65%);
}
.team-card h4 {
font-size: 0.92rem;
font-weight: 700;
color: var(--text);
margin-bottom: 0.3rem;
}
.team-card span {
font-size: 0.78rem;
color: var(--muted);
}
/* ── CTA ── */
.cta-section {
padding: 8rem 2rem;
text-align: center;
border-top: 1px solid var(--border);
background: radial-gradient(ellipse at 50% 50%, rgba(134, 232, 255, 0.06) 0%, transparent 70%);
}
.cta-section h2 {
font-size: 3rem;
font-weight: 900;
margin-bottom: 1rem;
letter-spacing: -1px;
}
.cta-section p {
font-size: 1.1rem;
color: var(--muted);
margin-bottom: 2rem;
}
/* ── Responsive ── */
@media (max-width: 900px) {
.charts-grid {
grid-template-columns: 1fr;
}
.chart-wide {
grid-column: 1;
}
}
@media (max-width: 768px) {
.hero-title {
font-size: 2.8rem;
}
.section-title {
font-size: 2rem;
}
.metrics-grid {
grid-template-columns: repeat(2, 1fr);
}
.timeline::before {
left: 1.2rem;
}
.tl-item,
.tl-item[data-side="right"] {
flex-direction: row;
}
.tl-dot {
left: 1.2rem;
}
.tl-card {
width: calc(100% - 4rem);
margin-left: 3rem !important;
margin-right: 0 !important;
}
.team-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 480px) {
.hero-title {
font-size: 2.2rem;
}
.metrics-grid {
grid-template-columns: 1fr;
}
.team-grid {
grid-template-columns: repeat(2, 1fr);
}
.cta-section h2 {
font-size: 2rem;
}
}
html.reduced-motion .gradient-text {
animation: none !important;
}
html.reduced-motion * {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}if (!window.MotionPreference) {
const __mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const __listeners = new Set();
const MotionPreference = {
prefersReducedMotion() {
return __mql.matches;
},
setOverride(value) {
const reduced = Boolean(value);
document.documentElement.classList.toggle("reduced-motion", reduced);
window.dispatchEvent(new CustomEvent("motion-preference", { detail: { reduced } }));
for (const listener of __listeners) {
try {
listener({ reduced, override: reduced, systemReduced: __mql.matches });
} catch {}
}
},
onChange(listener) {
__listeners.add(listener);
try {
listener({
reduced: __mql.matches,
override: null,
systemReduced: __mql.matches,
});
} catch {}
return () => __listeners.delete(listener);
},
getState() {
return { reduced: __mql.matches, override: null, systemReduced: __mql.matches };
},
};
window.MotionPreference = MotionPreference;
}
function prefersReducedMotion() {
return window.MotionPreference.prefersReducedMotion();
}
function initDemoShell() {
// No-op shim in imported standalone snippets.
}
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { TextPlugin } from "gsap/TextPlugin";
import Lenis from "lenis";
gsap.registerPlugin(ScrollTrigger, TextPlugin);
initDemoShell({
title: "Data Dashboard",
category: "pages",
tech: ["gsap", "canvas-2d", "scrolltrigger", "lenis", "textplugin"],
});
const lenis = new Lenis({ lerp: 0.1, smoothWheel: true });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
const reduced = prefersReducedMotion();
if (reduced) document.documentElement.classList.add("reduced-motion");
// ─── Hero ───────────────────────────────────────────────────────────────────
if (!reduced) {
const heroTl = gsap.timeline({ defaults: { ease: "expo.out" } });
gsap.set([".hero-badge", ".hero-title", ".hero-sub", ".hero-cta"], { opacity: 0, y: 30 });
heroTl
.to(".hero-badge", { opacity: 1, y: 0, duration: 0.6, delay: 0.4 })
.to(".hero-title", { opacity: 1, y: 0, duration: 1 }, "-=0.3")
.to(".hero-sub", { opacity: 1, y: 0, duration: 0.7 }, "-=0.5")
.to(".hero-cta", { opacity: 1, y: 0, duration: 0.6 }, "-=0.4");
}
// Gradient hue shift on scroll — hero title (Demo 37 technique)
if (!reduced) {
ScrollTrigger.create({
trigger: document.body,
start: "top top",
end: "bottom bottom",
onUpdate: (self) => {
const hue = self.progress * 360;
document.documentElement.style.setProperty("--hue-shift", `${hue}deg`);
},
});
}
// ─── Metrics Counters (Demo 36 technique) ───────────────────────────────────
const metricCards = document.querySelectorAll(".metric-card");
metricCards.forEach((card, i) => {
const valueEl = card.querySelector(".metric-value");
const target = parseFloat(card.dataset.target);
const suffix = card.dataset.suffix || "";
let currentValue = 0;
if (!reduced) {
// Card entrance
gsap.set(card, { opacity: 0, y: 40, scale: 0.95 });
gsap.to(card, {
opacity: 1,
y: 0,
scale: 1,
duration: 0.8,
ease: "expo.out",
delay: i * 0.07,
scrollTrigger: {
trigger: ".metrics-grid",
start: "top 75%",
toggleActions: "play none none reverse",
},
});
// Progress bar fill
const bar = card.querySelector(".metric-bar-fill");
if (bar) {
gsap.set(bar, { scaleX: 0, transformOrigin: "left center" });
gsap.to(bar, {
scaleX: 1,
duration: 1.2,
ease: "expo.out",
delay: i * 0.1 + 0.5,
scrollTrigger: {
trigger: ".metrics-grid",
start: "top 75%",
toggleActions: "play none none reverse",
},
});
}
// Counter animation scrubbed to scroll
ScrollTrigger.create({
trigger: card,
start: "top 80%",
end: "top 20%",
onUpdate: (self) => {
const progress = self.progress;
currentValue = target * progress;
let display;
if (target >= 1000) {
display = Math.round(currentValue).toLocaleString();
} else if (target < 10) {
display = currentValue.toFixed(1);
} else {
display = Math.round(currentValue);
}
valueEl.textContent = display + suffix;
},
});
} else {
valueEl.textContent = target >= 1000 ? target.toLocaleString() + suffix : target + suffix;
}
});
// ─── Section Headers ─────────────────────────────────────────────────────────
document.querySelectorAll(".section-header").forEach((header) => {
if (!reduced) {
gsap.set(header.children, { opacity: 0, y: 30 });
gsap.to(header.children, {
opacity: 1,
y: 0,
duration: 0.8,
ease: "expo.out",
stagger: 0.1,
scrollTrigger: { trigger: header, start: "top 75%", toggleActions: "play none none reverse" },
});
}
});
// ─── Canvas Charts ───────────────────────────────────────────────────────────
function animateBarChart(canvas, progress) {
const ctx = canvas.getContext("2d");
const W = canvas.width;
const H = canvas.height;
const data2024 = [22, 28, 25, 32, 30, 38, 35, 41, 38, 44, 42, 48];
const data2025 = [48, 52, 45, 58, 62, 55, 68, 72, 65, 78, 82, 90];
const months = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"];
const padding = { top: 16, right: 16, bottom: 32, left: 32 };
const chartW = W - padding.left - padding.right;
const chartH = H - padding.top - padding.bottom;
const maxVal = Math.max(...data2025) * 1.1;
const barGroupW = chartW / data2024.length;
const barW = barGroupW * 0.35;
ctx.clearRect(0, 0, W, H);
ctx.font = "10px -apple-system, sans-serif";
ctx.fillStyle = "rgba(138, 149, 168, 0.5)";
// Grid lines
for (let i = 0; i <= 4; i++) {
const y = padding.top + chartH - (chartH * i) / 4;
ctx.beginPath();
ctx.strokeStyle = "rgba(38, 50, 73, 0.8)";
ctx.lineWidth = 1;
ctx.moveTo(padding.left, y);
ctx.lineTo(W - padding.right, y);
ctx.stroke();
ctx.fillText(Math.round((maxVal * i) / 4), 2, y + 4);
}
// Bars
data2024.forEach((val, i) => {
const x = padding.left + i * barGroupW;
const barH2024 = (val / maxVal) * chartH * Math.min(1, progress * 1.5);
const barH2025 = (data2025[i] / maxVal) * chartH * Math.min(1, progress * 1.5);
// 2024 bar
ctx.fillStyle = "rgba(134, 232, 255, 0.35)";
ctx.fillRect(x + barGroupW * 0.1, padding.top + chartH - barH2024, barW, barH2024);
// 2025 bar
ctx.fillStyle = "rgba(174, 82, 255, 0.7)";
ctx.fillRect(x + barGroupW * 0.1 + barW + 2, padding.top + chartH - barH2025, barW, barH2025);
// Month label
ctx.fillStyle = "rgba(138, 149, 168, 0.7)";
ctx.fillText(months[i], x + barGroupW * 0.3, H - 8);
});
}
function animateDonutChart(canvas, progress) {
const ctx = canvas.getContext("2d");
const W = canvas.width;
const H = canvas.height;
const cx = W / 2;
const cy = H / 2;
const outerR = Math.min(W, H) * 0.42;
const innerR = outerR * 0.58;
const data = [
{ val: 0.62, color: "#86e8ff", label: "SaaS" },
{ val: 0.28, color: "#ae52ff", label: "Enterprise" },
{ val: 0.1, color: "#ffcc66", label: "Add-ons" },
];
ctx.clearRect(0, 0, W, H);
let startAngle = -Math.PI / 2;
data.forEach((seg) => {
const sweep = seg.val * 2 * Math.PI * Math.min(1, progress);
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, outerR, startAngle, startAngle + sweep);
ctx.arc(cx, cy, innerR, startAngle + sweep, startAngle, true);
ctx.closePath();
ctx.fillStyle = seg.color;
ctx.globalAlpha = 0.85;
ctx.fill();
ctx.globalAlpha = 1;
startAngle += sweep;
});
// Center text
ctx.font = `bold 18px -apple-system, sans-serif`;
ctx.fillStyle = "#f0f4fb";
ctx.textAlign = "center";
ctx.fillText("Revenue", cx, cy - 4);
ctx.font = `12px -apple-system, sans-serif`;
ctx.fillStyle = "#8a95a8";
ctx.fillText("Q4 2025", cx, cy + 14);
}
function animateAreaChart(canvas, progress) {
const ctx = canvas.getContext("2d");
const W = canvas.width;
const H = canvas.height;
const padding = { top: 16, right: 16, bottom: 24, left: 40 };
const chartW = W - padding.left - padding.right;
const chartH = H - padding.top - padding.bottom;
// Generate random-ish API request data
const points = Array.from({ length: 30 }, (_, i) => {
const base = 2.5 + Math.sin(i * 0.4) * 0.6;
const noise = Math.sin(i * 1.7 + 2) * 0.3 + Math.sin(i * 0.9) * 0.2;
return base + noise;
});
const maxVal = Math.max(...points) * 1.1;
ctx.clearRect(0, 0, W, H);
// Grid
ctx.strokeStyle = "rgba(38, 50, 73, 0.6)";
ctx.lineWidth = 1;
for (let i = 0; i <= 3; i++) {
const y = padding.top + (chartH * i) / 3;
ctx.beginPath();
ctx.moveTo(padding.left, y);
ctx.lineTo(W - padding.right, y);
ctx.stroke();
ctx.font = "9px -apple-system, sans-serif";
ctx.fillStyle = "rgba(138, 149, 168, 0.6)";
ctx.fillText(`${(maxVal - (maxVal * i) / 3).toFixed(1)}M`, 2, y + 4);
}
const visiblePoints = Math.max(2, Math.round(points.length * progress));
const step = chartW / (points.length - 1);
// Area fill
ctx.beginPath();
ctx.moveTo(padding.left, padding.top + chartH);
points.slice(0, visiblePoints).forEach((val, i) => {
const x = padding.left + i * step;
const y = padding.top + chartH - (val / maxVal) * chartH;
if (i === 0) ctx.lineTo(x, y);
else ctx.lineTo(x, y);
});
ctx.lineTo(padding.left + (visiblePoints - 1) * step, padding.top + chartH);
ctx.closePath();
const grad = ctx.createLinearGradient(0, padding.top, 0, padding.top + chartH);
grad.addColorStop(0, "rgba(134, 232, 255, 0.25)");
grad.addColorStop(1, "rgba(134, 232, 255, 0)");
ctx.fillStyle = grad;
ctx.fill();
// Line
ctx.beginPath();
points.slice(0, visiblePoints).forEach((val, i) => {
const x = padding.left + i * step;
const y = padding.top + chartH - (val / maxVal) * chartH;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.strokeStyle = "#86e8ff";
ctx.lineWidth = 2;
ctx.stroke();
}
// Chart animation on scroll
const barCanvas = document.getElementById("bar-chart");
const donutCanvas = document.getElementById("donut-chart");
const areaCanvas = document.getElementById("area-chart");
// Initialize charts at 0
animateBarChart(barCanvas, 0);
animateDonutChart(donutCanvas, 0);
animateAreaChart(areaCanvas, 0);
if (!reduced) {
// Chart cards entrance
document.querySelectorAll(".chart-card").forEach((card, i) => {
gsap.set(card, { opacity: 0, y: 40 });
gsap.to(card, {
opacity: 1,
y: 0,
duration: 0.8,
ease: "expo.out",
delay: i * 0.1,
scrollTrigger: {
trigger: ".charts-grid",
start: "top 75%",
toggleActions: "play none none reverse",
},
});
});
// Bar chart animated on scroll
ScrollTrigger.create({
trigger: barCanvas,
start: "top 80%",
end: "top 10%",
onUpdate: (self) => animateBarChart(barCanvas, self.progress),
});
// Donut chart
ScrollTrigger.create({
trigger: donutCanvas,
start: "top 80%",
end: "top 10%",
onUpdate: (self) => animateDonutChart(donutCanvas, self.progress),
});
// Area chart
ScrollTrigger.create({
trigger: areaCanvas,
start: "top 85%",
end: "top 20%",
onUpdate: (self) => animateAreaChart(areaCanvas, self.progress),
});
} else {
// Show full charts immediately for reduced motion
animateBarChart(barCanvas, 1);
animateDonutChart(donutCanvas, 1);
animateAreaChart(areaCanvas, 1);
}
// ─── Timeline ────────────────────────────────────────────────────────────────
document.querySelectorAll(".tl-item").forEach((item, i) => {
if (!reduced) {
const side = item.dataset.side === "left" ? -40 : 40;
gsap.set(item, { opacity: 0, x: side });
gsap.to(item, {
opacity: 1,
x: 0,
duration: 0.8,
ease: "expo.out",
scrollTrigger: { trigger: item, start: "top 75%", toggleActions: "play none none reverse" },
});
}
});
// ─── Team Grid ───────────────────────────────────────────────────────────────
document.querySelectorAll(".team-card").forEach((card, i) => {
if (!reduced) {
gsap.set(card, { opacity: 0, scale: 0.85, y: 20 });
gsap.to(card, {
opacity: 1,
scale: 1,
y: 0,
duration: 0.7,
ease: "back.out(1.5)",
delay: i * 0.08,
scrollTrigger: {
trigger: ".team-grid",
start: "top 75%",
toggleActions: "play none none reverse",
},
});
}
});
// ─── CTA ─────────────────────────────────────────────────────────────────────
if (!reduced) {
gsap.set(".cta-section h2, .cta-section p, .cta-section a", { opacity: 0, y: 30 });
gsap.to(".cta-section h2, .cta-section p, .cta-section a", {
opacity: 1,
y: 0,
duration: 0.8,
stagger: 0.12,
ease: "expo.out",
scrollTrigger: {
trigger: ".cta-section",
start: "top 75%",
toggleActions: "play none none reverse",
},
});
}
// ─── Motion preference ───────────────────────────────────────────────────────
window.addEventListener("motion-preference", (e) => {
if (e.detail.reduced) {
gsap.globalTimeline.paused(true);
animateBarChart(barCanvas, 1);
animateDonutChart(donutCanvas, 1);
animateAreaChart(areaCanvas, 1);
} else {
gsap.globalTimeline.paused(false);
}
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Dashboard — stealthisdesign</title>
<link rel="stylesheet" href="style.css">
<script type="importmap">{"imports":{"gsap":"https://esm.sh/gsap@3.13.0","gsap/ScrollTrigger":"https://esm.sh/gsap@3.13.0/ScrollTrigger","gsap/SplitText":"https://esm.sh/gsap@3.13.0/SplitText","gsap/Flip":"https://esm.sh/gsap@3.13.0/Flip","gsap/ScrambleTextPlugin":"https://esm.sh/gsap@3.13.0/ScrambleTextPlugin","gsap/TextPlugin":"https://esm.sh/gsap@3.13.0/TextPlugin","gsap/all":"https://esm.sh/gsap@3.13.0/all","gsap/":"https://esm.sh/gsap@3.13.0/","lenis":"https://esm.sh/lenis@1.1.13/dist/lenis.mjs","three":"https://esm.sh/three@0.171.0","three/addons/":"https://esm.sh/three@0.171.0/examples/jsm/"}}</script>
<style>html.lenis,
html.lenis body {
height: auto;
}
.lenis:not(.lenis-autoToggle).lenis-stopped {
overflow: clip;
}
.lenis [data-lenis-prevent],
.lenis [data-lenis-prevent-wheel],
.lenis [data-lenis-prevent-touch] {
overscroll-behavior: contain;
}
.lenis.lenis-smooth iframe {
pointer-events: none;
}
.lenis.lenis-autoToggle {
transition-property: overflow;
transition-duration: 1ms;
transition-behavior: allow-discrete;
}</style>
</head>
<body>
<!-- Hero -->
<section class="hero">
<div class="hero-bg" aria-hidden="true"></div>
<div class="hero-content">
<div class="hero-badge">Startup Metrics 2025</div>
<h1 class="hero-title gradient-text">Growth at a Glance</h1>
<p class="hero-sub">Real-time analytics for product teams who ship fast. Every metric is animated as you scroll — counters, charts, and timelines come alive.</p>
<div class="hero-cta">
<a href="#metrics" class="btn-primary">View Metrics</a>
<a href="#charts" class="btn-ghost">See Charts</a>
</div>
</div>
<div class="hero-scroll-hint" aria-hidden="true">
<span>Scroll</span>
<div class="scroll-arrow"></div>
</div>
</section>
<!-- Metrics Strip -->
<section class="metrics-section" id="metrics">
<div class="section-header">
<span class="section-tag">Key Metrics</span>
<h2 class="section-title">Numbers that matter</h2>
<p class="section-desc">Scroll through to watch each metric count up from zero — linked directly to your scroll position.</p>
</div>
<div class="metrics-grid">
<div class="metric-card" data-target="48200" data-suffix="+" data-label="Active Users" data-sub="+12% this month">
<div class="metric-icon">◆</div>
<div class="metric-value">0</div>
<div class="metric-label">Active Users</div>
<div class="metric-sub">+12% this month</div>
<div class="metric-bar"><div class="metric-bar-fill" style="--bar-w: 82%"></div></div>
</div>
<div class="metric-card" data-target="99.7" data-suffix="%" data-label="Uptime SLA" data-sub="Last 90 days">
<div class="metric-icon">▲</div>
<div class="metric-value">0</div>
<div class="metric-label">Uptime SLA</div>
<div class="metric-sub">Last 90 days</div>
<div class="metric-bar"><div class="metric-bar-fill" style="--bar-w: 99%"></div></div>
</div>
<div class="metric-card" data-target="3.2" data-suffix="M" data-label="API Requests/Day" data-sub="Peak: 4.1M">
<div class="metric-icon">●</div>
<div class="metric-value">0</div>
<div class="metric-label">API Req/Day</div>
<div class="metric-sub">Peak: 4.1M</div>
<div class="metric-bar"><div class="metric-bar-fill" style="--bar-w: 67%"></div></div>
</div>
<div class="metric-card" data-target="4.8" data-suffix="" data-label="App Store Rating" data-sub="23k reviews">
<div class="metric-icon">★</div>
<div class="metric-value">0</div>
<div class="metric-label">App Rating</div>
<div class="metric-sub">23k reviews</div>
<div class="metric-bar"><div class="metric-bar-fill" style="--bar-w: 96%"></div></div>
</div>
<div class="metric-card" data-target="128" data-suffix=" ms" data-label="Avg Response Time" data-sub="p95: 380ms">
<div class="metric-icon">◈</div>
<div class="metric-value">0</div>
<div class="metric-label">Avg Response</div>
<div class="metric-sub">p95: 380ms</div>
<div class="metric-bar"><div class="metric-bar-fill" style="--bar-w: 55%"></div></div>
</div>
<div class="metric-card" data-target="6" data-suffix="" data-label="Team Members" data-sub="Across 4 time zones">
<div class="metric-icon">▣</div>
<div class="metric-value">0</div>
<div class="metric-label">Team Size</div>
<div class="metric-sub">4 time zones</div>
<div class="metric-bar"><div class="metric-bar-fill" style="--bar-w: 40%"></div></div>
</div>
</div>
</section>
<!-- Charts Section -->
<section class="charts-section" id="charts">
<div class="section-header">
<span class="section-tag">Analytics</span>
<h2 class="section-title gradient-text">Visualized Growth</h2>
<p class="section-desc">Canvas 2D charts animate into view on scroll. Bar charts, area charts, and donut charts — all built without a third-party chart library.</p>
</div>
<div class="charts-grid">
<div class="chart-card">
<div class="chart-header">
<h3>Monthly Active Users</h3>
<span class="chart-badge trend-up">↑ 23%</span>
</div>
<canvas id="bar-chart" width="480" height="220" aria-hidden="true"></canvas>
<div class="chart-legend">
<span class="legend-item"><span class="dot accent"></span>2024</span>
<span class="legend-item"><span class="dot purple"></span>2025</span>
</div>
</div>
<div class="chart-card">
<div class="chart-header">
<h3>Revenue Breakdown</h3>
<span class="chart-badge">Q4 2025</span>
</div>
<canvas id="donut-chart" width="280" height="220" aria-hidden="true"></canvas>
<div class="donut-legend">
<div class="dl-item"><span class="dl-dot" style="background: #86e8ff"></span><span>SaaS Plans</span><strong>62%</strong></div>
<div class="dl-item"><span class="dl-dot" style="background: #ae52ff"></span><span>Enterprise</span><strong>28%</strong></div>
<div class="dl-item"><span class="dl-dot" style="background: #ffcc66"></span><span>Add-ons</span><strong>10%</strong></div>
</div>
</div>
<div class="chart-card chart-wide">
<div class="chart-header">
<h3>Daily API Requests (Last 30 Days)</h3>
<span class="chart-badge trend-up">↑ Peak 4.1M</span>
</div>
<canvas id="area-chart" width="900" height="180" aria-hidden="true"></canvas>
</div>
</div>
</section>
<!-- Timeline -->
<section class="timeline-section">
<div class="section-header">
<span class="section-tag">Roadmap</span>
<h2 class="section-title">Milestones</h2>
</div>
<div class="timeline">
<div class="tl-item" data-side="left">
<div class="tl-dot"></div>
<div class="tl-card">
<span class="tl-date">Q1 2024</span>
<h4>Private Beta</h4>
<p>Launched with 50 design partners. Core analytics pipeline and dashboard shipped.</p>
</div>
</div>
<div class="tl-item" data-side="right">
<div class="tl-dot"></div>
<div class="tl-card">
<span class="tl-date">Q2 2024</span>
<h4>Public Launch</h4>
<p>2,400 signups in first week. Featured in Product Hunt Top 5 of the day.</p>
</div>
</div>
<div class="tl-item" data-side="left">
<div class="tl-dot"></div>
<div class="tl-card">
<span class="tl-date">Q4 2024</span>
<h4>Series A — $4.2M</h4>
<p>Raised seed round to expand team and infrastructure. Hired VP of Engineering.</p>
</div>
</div>
<div class="tl-item" data-side="right">
<div class="tl-dot tl-dot--active"></div>
<div class="tl-card tl-card--active">
<span class="tl-date">Now</span>
<h4>48,200 Active Users</h4>
<p>Scaling infrastructure and preparing for enterprise tier launch in Q1 2026.</p>
</div>
</div>
</div>
</section>
<!-- Team Section -->
<section class="team-section">
<div class="section-header">
<span class="section-tag">The Team</span>
<h2 class="section-title">Built by 6</h2>
<p class="section-desc">A small team with a massive impact. Remote-first, async-first, quality-first.</p>
</div>
<div class="team-grid">
<div class="team-card">
<div class="team-avatar" style="--av-hue: 195">AK</div>
<h4>Alex Kim</h4>
<span>CEO / Product</span>
</div>
<div class="team-card">
<div class="team-avatar" style="--av-hue: 270">MR</div>
<h4>Maria Reyes</h4>
<span>CTO</span>
</div>
<div class="team-card">
<div class="team-avatar" style="--av-hue: 330">JS</div>
<h4>Jordan Sato</h4>
<span>Design Lead</span>
</div>
<div class="team-card">
<div class="team-avatar" style="--av-hue: 45">PE</div>
<h4>Priya Elara</h4>
<span>Full-stack Eng</span>
</div>
<div class="team-card">
<div class="team-avatar" style="--av-hue: 150">TN</div>
<h4>Tom Nkosi</h4>
<span>Data Engineer</span>
</div>
<div class="team-card">
<div class="team-avatar" style="--av-hue: 15">LV</div>
<h4>Lena Voss</h4>
<span>Growth</span>
</div>
</div>
</section>
<!-- CTA -->
<section class="cta-section">
<h2 class="gradient-text">Ready to track yours?</h2>
<p>Get started with a free 14-day trial. No credit card required.</p>
<a href="/" class="btn-primary">Start Free Trial</a>
</section>
<script type="module" src="script.js"></script>
</body>
</html>Data Dashboard
Startup metrics dashboard with scroll-linked number counters, gradient text hue-shift hero, animated Canvas 2D bar/donut/area charts, a milestone timeline, and team grid.
Source
- Repository:
libs-genclaude - Original demo id:
41-data-dashboard
Notes
Startup metrics dashboard with scroll-linked number counters, gradient text hue-shift hero, animated Canvas 2D bar/donut/area charts, a milestone timeline, and team grid.