Data visualization bukan hanya tentang menampilkan angka—ini tentang menceritakan kisah melalui animasi yang bermakna. Dalam tutorial ini, kita akan belajar cara membuat dashboard interaktif dengan GSAP yang menggabungkan:
- Animated Counter (Angka yang "bertumbuh" secara smooth).
- Morphing SVG Charts (Grafik yang berubah bentuk/warna saat scroll).
- Text Reveal Animation (Teks muncul huruf demi huruf).
- Progress Bars dengan Wave Effect (Progress bar bergelombang).
- Interactive Data Cards (Kartu yang responsif terhadap mouse).
Kami akan membuat dashboard "Analytics Hub" fiktif yang memvisualisasikan metrik performa produk.
Kebutuhan
- Pemahaman dasar HTML, CSS, Tailwind.
- GSAP versi 3.12+.
- Editor kode (VS Code).
Langkah 1: Setup & Animated Counter

Counter yang beranimasi adalah cara sempurna untuk menarik perhatian pengguna pada angka penting.
Konsep:
Kita menggunakan gsap.to() dengan callback function untuk mengupdate teks counter setiap frame. GSAP akan menghitung dari nilai awal ke nilai akhir secara smooth.
file: hero.html
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Langkah 1: Hero & Animated Counter</title>
<!-- 1. External Libraries -->
<script src="<https://cdn.tailwindcss.com>"></script>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js>"></script>
<style>
@import url("<https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap>");
body {
font-family: "Inter", sans-serif;
}
/* Custom Grid Pattern */
.bg-grid {
background-size: 40px 40px;
background-image: linear-gradient(
to right,
rgba(255, 255, 255, 0.05) 1px,
transparent 1px
),
linear-gradient(
to bottom,
rgba(255, 255, 255, 0.05) 1px,
transparent 1px
);
}
/* Prevent FOUC (Flash of Unstyled Content) */
.hero-anim,
.card-anim {
visibility: hidden;
}
</style>
</head>
<body
class="bg-[#020617] text-white overflow-x-hidden selection:bg-indigo-500/30"
>
<!-- HERO SECTION -->
<section
class="relative min-h-screen flex flex-col items-center justify-center py-20"
>
<!-- Background Elements -->
<div class="absolute inset-0 bg-[#020617] -z-10 overflow-hidden">
<div class="absolute inset-0 bg-grid opacity-[0.4]"></div>
<div
class="absolute inset-0 bg-gradient-to-t from-[#020617] via-transparent to-transparent"
></div>
<!-- Animated Blobs -->
<div
class="absolute top-0 -left-40 w-96 h-96 bg-indigo-500/30 rounded-full mix-blend-screen filter blur-[128px] opacity-50 animate-pulse"
></div>
<div
class="absolute bottom-0 -right-40 w-96 h-96 bg-cyan-500/30 rounded-full mix-blend-screen filter blur-[128px] opacity-50 animate-pulse"
style="animation-delay: 2s"
></div>
</div>
<!-- Content -->
<div class="relative z-10 container mx-auto px-6 text-center">
<!-- Badge -->
<div
class="hero-anim inline-flex items-center gap-2 px-3 py-1 rounded-full border border-indigo-500/30 bg-indigo-500/10 text-indigo-300 text-xs font-medium mb-8 backdrop-blur-md"
>
<span class="relative flex h-2 w-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-indigo-400 opacity-75"
></span>
<span
class="relative inline-flex rounded-full h-2 w-2 bg-indigo-500"
></span>
</span>
Live Data Dashboard
</div>
<!-- Main Title -->
<h1
class="hero-anim text-5xl md:text-7xl lg:text-8xl font-black tracking-tight mb-6 leading-tight"
>
Analytics
<span
class="text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 via-cyan-400 to-emerald-400"
>Hub</span
>
</h1>
<p
class="hero-anim text-lg md:text-xl text-slate-400 mb-12 max-w-2xl mx-auto leading-relaxed"
>
Platform visualisasi data real-time yang mengubah angka kompleks
menjadi cerita yang dapat ditindaklanjuti.
</p>
<!-- Counter Cards Grid -->
<div
class="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto mt-16 w-full"
>
<!-- Card 1 -->
<div
class="card-anim group relative p-8 rounded-3xl border border-white/5 bg-white/[0.02] hover:bg-white/[0.05] backdrop-blur-xl transition-all duration-500 hover:-translate-y-2 hover:shadow-[0_20px_40px_-15px_rgba(99,102,241,0.3)] text-left"
>
<div
class="absolute inset-0 rounded-3xl border border-indigo-500/0 group-hover:border-indigo-500/50 transition-colors duration-500"
></div>
<div class="relative z-10">
<div
class="w-12 h-12 mb-4 rounded-2xl bg-indigo-500/20 flex items-center justify-center text-indigo-400"
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
></path>
</svg>
</div>
<p class="text-slate-400 text-sm font-medium mb-1">Total Users</p>
<div
class="text-4xl font-bold text-white counter"
data-target="125000"
>
0
</div>
<div
class="flex items-center gap-1 mt-3 text-emerald-400 text-xs font-medium bg-emerald-400/10 w-fit px-2 py-1 rounded-lg"
>
<span>+12% growth</span>
</div>
</div>
</div>
<!-- Card 2 -->
<div
class="card-anim group relative p-8 rounded-3xl border border-white/5 bg-white/[0.02] hover:bg-white/[0.05] backdrop-blur-xl transition-all duration-500 hover:-translate-y-2 hover:shadow-[0_20px_40px_-15px_rgba(6,182,212,0.3)] text-left"
>
<div
class="absolute inset-0 rounded-3xl border border-cyan-500/0 group-hover:border-cyan-500/50 transition-colors duration-500"
></div>
<div class="relative z-10">
<div
class="w-12 h-12 mb-4 rounded-2xl bg-cyan-500/20 flex items-center justify-center text-cyan-400"
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</div>
<p class="text-slate-400 text-sm font-medium mb-1">
Total Revenue
</p>
<div class="flex items-baseline gap-2">
<span class="text-2xl text-cyan-400 font-semibold">$</span>
<div
class="text-4xl font-bold text-white counter"
data-target="2500000"
>
0
</div>
</div>
<div
class="flex items-center gap-1 mt-3 text-emerald-400 text-xs font-medium bg-emerald-400/10 w-fit px-2 py-1 rounded-lg"
>
<span>+28% vs last Q</span>
</div>
</div>
</div>
<!-- Card 3 -->
<div
class="card-anim group relative p-8 rounded-3xl border border-white/5 bg-white/[0.02] hover:bg-white/[0.05] backdrop-blur-xl transition-all duration-500 hover:-translate-y-2 hover:shadow-[0_20px_40px_-15px_rgba(16,185,129,0.3)] text-left"
>
<div
class="absolute inset-0 rounded-3xl border border-emerald-500/0 group-hover:border-emerald-500/50 transition-colors duration-500"
></div>
<div class="relative z-10">
<div
class="w-12 h-12 mb-4 rounded-2xl bg-emerald-500/20 flex items-center justify-center text-emerald-400"
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
></path>
</svg>
</div>
<p class="text-slate-400 text-sm font-medium mb-1">
Conversion Rate
</p>
<div class="flex items-baseline gap-1">
<div
class="text-4xl font-bold text-white counter"
data-target="8.5"
>
0
</div>
<span class="text-2xl text-emerald-400 font-semibold">%</span>
</div>
<div
class="flex items-center gap-1 mt-3 text-indigo-400 text-xs font-medium bg-indigo-400/10 w-fit px-2 py-1 rounded-lg"
>
<span>92% target hit</span>
</div>
</div>
</div>
</div>
</div>
</section>
<script>
gsap.registerPlugin(ScrollTrigger);
// 1. ENTRANCE ANIMATION (Muncul dari bawah)
const tl = gsap.timeline();
tl.to(".hero-anim", {
autoAlpha: 1,
y: 0,
duration: 1,
stagger: 0.2,
ease: "power3.out",
startAt: { y: 30, autoAlpha: 0 },
}).to(
".card-anim",
{
autoAlpha: 1,
y: 0,
duration: 1,
stagger: 0.2,
ease: "back.out(1.7)",
startAt: { y: 50, autoAlpha: 0 },
onComplete: startCounters, // Panggil counter setelah kartu muncul
},
"-=0.5"
);
// 2. COUNTER LOGIC
function startCounters() {
const counters = document.querySelectorAll(".counter");
counters.forEach((counter) => {
const target = parseFloat(counter.getAttribute("data-target"));
const isDecimal = target % 1 !== 0;
gsap.to(counter, {
textContent: target,
duration: 2.5,
ease: "power2.out",
snap: { textContent: isDecimal ? 0.1 : 1 },
onUpdate: function () {
let value = parseFloat(this.targets()[0].textContent);
counter.textContent = isDecimal
? value.toFixed(1)
: Math.floor(value).toLocaleString("en-US");
},
});
});
}
</script>
</body>
</html>
Penjelasan Kode:
- Struktur: Memuat library eksternal (Tailwind, GSAP, ScrollTrigger).
- Styling: Menambahkan
bg-griduntuk tekstur background dan mencegah FOUC (Flash of Unstyled Content) menggunakan CSSvisibility: hidden. - Logika:
- Entrance Animation: Menggunakan
gsap.timeline()untuk memunculkan elemen secara berurutan dari bawah ke atas. - Counter Logic: Fungsi
startCountersdipanggil tepat setelah animasi masuk selesai (onComplete), memastikan angka mulai bergerak hanya ketika pengguna sudah melihat kartunya.
- Entrance Animation: Menggunakan
Langkah 2: Progress Bar dengan Wave Effect

Progress bar bukan hanya garis horizontal kita akan membuat efek bergelombang.
File 2: progress.html
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Langkah 2: Progress Bar Wave Optimized</title>
<script src="<https://cdn.tailwindcss.com>"></script>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js>"></script>
<style>
@import url("<https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap>");
body {
font-family: "Inter", sans-serif;
}
@keyframes wave-slide {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.animate-wave {
width: 200%;
animation: wave-slide 2s linear infinite;
will-change: transform;
}
</style>
</head>
<body
class="bg-[#020617] text-white overflow-x-hidden selection:bg-indigo-500/30"
>
<!-- PROGRESS BAR SECTION -->
<section
class="relative min-h-screen flex items-center justify-center py-20 px-6"
>
<!-- Background Glow -->
<div
class="absolute right-0 top-1/4 w-96 h-96 bg-indigo-600/10 rounded-full blur-[100px] pointer-events-none"
></div>
<div class="relative z-10 container mx-auto max-w-4xl">
<!-- Header -->
<div class="mb-16 text-center md:text-left">
<h2 class="text-4xl md:text-5xl font-black mb-4">
Product <span class="text-indigo-400">Metrics</span>
</h2>
<p class="text-slate-400 text-lg max-w-2xl mx-auto md:mx-0">
Analisis mendalam terhadap performa sistem secara real-time.
</p>
</div>
<div
class="space-y-12 bg-white/[0.02] border border-white/5 p-8 md:p-12 rounded-3xl backdrop-blur-sm"
>
<div class="progress-item group">
<div class="flex justify-between items-end mb-3">
<div>
<h3 class="text-white font-bold text-lg">Performance Score</h3>
<p class="text-slate-500 text-sm">Sistem berjalan optimal</p>
</div>
<div class="text-right">
<span
class="text-3xl font-black text-white progress-value"
data-target="92"
>0</span
>
<span class="text-indigo-400 text-lg font-bold">%</span>
</div>
</div>
<div
class="h-4 bg-slate-800/50 rounded-full overflow-hidden border border-white/5 relative"
>
<div
class="progress-bar absolute top-0 left-0 h-full bg-gradient-to-r from-indigo-900 to-indigo-500 rounded-full w-0 shadow-[0_0_20px_rgba(99,102,241,0.5)]"
>
<div
class="absolute inset-0 opacity-30 h-full animate-wave flex"
>
<svg
class="w-1/2 h-full"
viewBox="0 0 100 10"
preserveAspectRatio="none"
>
<path d="M0 10 V5 Q25 0 50 5 T100 5 V10 Z" fill="white" />
</svg>
<svg
class="w-1/2 h-full"
viewBox="0 0 100 10"
preserveAspectRatio="none"
>
<path d="M0 10 V5 Q25 0 50 5 T100 5 V10 Z" fill="white" />
</svg>
</div>
<div
class="absolute right-0 top-0 bottom-0 w-1 bg-white/50 blur-[2px]"
></div>
</div>
</div>
</div>
<div class="progress-item group">
<div class="flex justify-between items-end mb-3">
<div>
<h3 class="text-white font-bold text-lg">User Satisfaction</h3>
<p class="text-slate-500 text-sm">Berdasarkan NPS survey</p>
</div>
<div class="text-right">
<span
class="text-3xl font-black text-white progress-value"
data-target="87"
>0</span
>
<span class="text-cyan-400 text-lg font-bold">/100</span>
</div>
</div>
<div
class="h-4 bg-slate-800/50 rounded-full overflow-hidden border border-white/5 relative"
>
<div
class="progress-bar absolute top-0 left-0 h-full bg-gradient-to-r from-cyan-900 to-cyan-500 rounded-full w-0 shadow-[0_0_20px_rgba(6,182,212,0.5)]"
>
<div
class="absolute inset-0 opacity-30 h-full animate-wave flex"
>
<svg
class="w-1/2 h-full"
viewBox="0 0 100 10"
preserveAspectRatio="none"
>
<path d="M0 10 V5 Q25 0 50 5 T100 5 V10 Z" fill="white" />
</svg>
<svg
class="w-1/2 h-full"
viewBox="0 0 100 10"
preserveAspectRatio="none"
>
<path d="M0 10 V5 Q25 0 50 5 T100 5 V10 Z" fill="white" />
</svg>
</div>
<div
class="absolute right-0 top-0 bottom-0 w-1 bg-white/50 blur-[2px]"
></div>
</div>
</div>
</div>
<div class="progress-item group">
<div class="flex justify-between items-end mb-3">
<div>
<h3 class="text-white font-bold text-lg">System Uptime</h3>
<p class="text-slate-500 text-sm">30 hari terakhir</p>
</div>
<div class="text-right">
<span
class="text-3xl font-black text-white progress-value"
data-target="99.9"
>0</span
>
<span class="text-emerald-400 text-lg font-bold">%</span>
</div>
</div>
<div
class="h-4 bg-slate-800/50 rounded-full overflow-hidden border border-white/5 relative"
>
<div
class="progress-bar absolute top-0 left-0 h-full bg-gradient-to-r from-emerald-900 to-emerald-500 rounded-full w-0 shadow-[0_0_20px_rgba(16,185,129,0.5)]"
>
<div
class="absolute inset-0 opacity-30 h-full animate-wave flex"
>
<svg
class="w-1/2 h-full"
viewBox="0 0 100 10"
preserveAspectRatio="none"
>
<path d="M0 10 V5 Q25 0 50 5 T100 5 V10 Z" fill="white" />
</svg>
<svg
class="w-1/2 h-full"
viewBox="0 0 100 10"
preserveAspectRatio="none"
>
<path d="M0 10 V5 Q25 0 50 5 T100 5 V10 Z" fill="white" />
</svg>
</div>
<div
class="absolute right-0 top-0 bottom-0 w-1 bg-white/50 blur-[2px]"
></div>
</div>
</div>
</div>
</div>
</div>
</section>
<script>
gsap.registerPlugin(ScrollTrigger);
const progressItems = document.querySelectorAll(".progress-item");
progressItems.forEach((item) => {
const bar = item.querySelector(".progress-bar");
const valueText = item.querySelector(".progress-value");
const targetValue = parseFloat(valueText.getAttribute("data-target"));
const isDecimal = targetValue % 1 !== 0;
ScrollTrigger.create({
trigger: item,
start: "top 85%",
onEnter: () => {
// 1. Animasi Lebar Bar
gsap.to(bar, {
width: `${targetValue}%`,
duration: 2,
ease: "power3.inOut",
});
gsap.to(valueText, {
textContent: targetValue,
duration: 2,
ease: "power3.inOut",
snap: { textContent: isDecimal ? 0.1 : 1 },
onUpdate: function () {
let val = parseFloat(this.targets()[0].textContent);
valueText.textContent = isDecimal
? val.toFixed(1)
: Math.floor(val);
},
});
},
once: true,
});
});
</script>
</body>
</html>
Penjelasan:
Berfokus pada visualisasi persentase dengan cara yang artistik.
- Struktur: Menggunakan layout yang konsisten dengan Langkah 1 (background gelap, tipografi Inter).
- Visual Effect (Wave):
- Kita menyisipkan elemen SVG (
<path>) di dalam bar untuk membuat bentuk gelombang. - CSS
@keyframes wave-movemenggeser gelombang tersebut secara infinite ke kiri, menciptakan ilusi cairan yang mengalir.
- Kita menyisipkan elemen SVG (
- Logika GSAP (
ScrollTrigger):- Animasi hanya berjalan ketika elemen masuk viewport (
start: "top 85%"). - Lebar bar (
width) dan angka persentase (textContent) dianimasikan secara bersamaan menggunakan easingpower3.inOutagar gerakannya halus (lambat-cepat-lambat).
- Animasi hanya berjalan ketika elemen masuk viewport (
Langkah 3: SVG Chart dengan Morphing Effect

Grafik yang berubah bentuk saat scroll adalah cara powerful untuk menunjukkan perubahan data.
File: morphing.html
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Langkah 3: Morphing SVG Chart</title>
<script src="<https://cdn.tailwindcss.com>"></script>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js>"></script>
<style>
@import url("<https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap>");
body {
font-family: "Inter", sans-serif;
}
/* Custom utility agar chart tidak terpotong */
.chart-overflow-visible {
overflow: visible;
}
</style>
</head>
<body
class="bg-[#020617] text-white overflow-x-hidden selection:bg-indigo-500/30"
>
<!-- CHART SECTION -->
<section
class="relative min-h-screen flex items-center justify-center py-20 px-6 overflow-hidden"
>
<!-- Background Accents -->
<div
class="absolute left-0 bottom-0 w-full h-1/2 bg-gradient-to-t from-indigo-900/10 to-transparent pointer-events-none"
></div>
<div class="container mx-auto max-w-6xl relative z-10">
<div class="mb-16 text-center md:text-left">
<h2 class="text-4xl md:text-5xl font-black mb-4 text-white">
Growth <span class="text-cyan-400">Trends</span>
</h2>
<p class="text-slate-400 text-lg">
Visualisasi pergerakan data historis dan proyeksi masa depan.
</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
<!-- CHART 1: Revenue Trend (Area Chart) -->
<div
class="bg-slate-900/40 border border-white/5 rounded-3xl p-8 backdrop-blur-xl relative group pl-16 pb-12"
>
<!-- Y-Axis Title -->
<div
class="absolute left-4 top-1/2 -translate-y-1/2 -rotate-90 text-xs font-bold tracking-widest text-slate-500 uppercase whitespace-nowrap origin-center"
>
Revenue (USD)
</div>
<!-- Header -->
<div class="flex justify-between items-start mb-6">
<div>
<h3 class="text-xl font-bold text-white">Monthly Revenue</h3>
<p class="text-sm text-slate-500">6 Bulan Terakhir</p>
</div>
<div class="p-2 bg-indigo-500/10 rounded-lg text-indigo-400">
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"
></path>
</svg>
</div>
</div>
<!-- SVG Container -->
<div class="relative h-64 w-full chart-container-line">
<!-- Grid Lines -->
<svg
class="absolute inset-0 w-full h-full text-slate-800"
preserveAspectRatio="none"
>
<line
x1="0"
y1="25%"
x2="100%"
y2="25%"
stroke="currentColor"
stroke-width="1"
stroke-dasharray="4 4"
/>
<line
x1="0"
y1="50%"
x2="100%"
y2="50%"
stroke="currentColor"
stroke-width="1"
stroke-dasharray="4 4"
/>
<line
x1="0"
y1="75%"
x2="100%"
y2="75%"
stroke="currentColor"
stroke-width="1"
stroke-dasharray="4 4"
/>
</svg>
<!-- Main Chart SVG -->
<svg
class="w-full h-full chart-overflow-visible"
viewBox="0 0 500 250"
preserveAspectRatio="none"
>
<defs>
<linearGradient
id="gradLine"
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop
offset="0%"
style="stop-color: #6366f1; stop-opacity: 0.5"
/>
<stop
offset="100%"
style="stop-color: #6366f1; stop-opacity: 0"
/>
</linearGradient>
</defs>
<!-- Area Path (Filled) -->
<!-- d: bentuk awal (datar di bawah) -->
<!-- data-d-to: bentuk akhir (kurva data) -->
<path
d="M0,250 L0,250 L83,250 L166,250 L250,250 L333,250 L416,250 L500,250 Z"
fill="url(#gradLine)"
class="morphing-area"
data-d-to="M0,250 L0,150 L83,180 L166,120 L250,160 L333,90 L416,110 L500,40 L500,250 Z"
/>
<!-- Stroke Path (Line only) -->
<path
d="M0,250 L83,250 L166,250 L250,250 L333,250 L416,250 L500,250"
fill="none"
stroke="#818cf8"
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
class="morphing-line"
data-d-to="M0,150 L83,180 L166,120 L250,160 L333,90 L416,110 L500,40"
/>
<!-- Data Points -->
<g class="data-points">
<circle
cx="0"
cy="150"
r="6"
fill="#fff"
stroke="#6366f1"
stroke-width="3"
/>
<circle
cx="83"
cy="180"
r="6"
fill="#fff"
stroke="#6366f1"
stroke-width="3"
/>
<circle
cx="166"
cy="120"
r="6"
fill="#fff"
stroke="#6366f1"
stroke-width="3"
/>
<circle
cx="250"
cy="160"
r="6"
fill="#fff"
stroke="#6366f1"
stroke-width="3"
/>
<circle
cx="333"
cy="90"
r="6"
fill="#fff"
stroke="#6366f1"
stroke-width="3"
/>
<circle
cx="416"
cy="110"
r="6"
fill="#fff"
stroke="#6366f1"
stroke-width="3"
/>
<circle
cx="500"
cy="40"
r="6"
fill="#fff"
stroke="#6366f1"
stroke-width="3"
/>
</g>
</svg>
</div>
<!-- X-Axis Title -->
<div
class="absolute bottom-4 left-1/2 -translate-x-1/2 text-xs font-bold tracking-widest text-slate-500 uppercase mt-4"
>
Timeline (Months)
</div>
</div>
<!-- CHART 2: User Engagement (Bar Chart) -->
<div
class="bg-slate-900/40 border border-white/5 rounded-3xl p-8 backdrop-blur-xl relative group pl-16 pb-12"
>
<!-- Y-Axis Title -->
<div
class="absolute left-4 top-1/2 -translate-y-1/2 -rotate-90 text-xs font-bold tracking-widest text-slate-500 uppercase whitespace-nowrap origin-center"
>
Active Users
</div>
<!-- Header -->
<div class="flex justify-between items-start mb-6">
<div>
<h3 class="text-xl font-bold text-white">User Engagement</h3>
<p class="text-sm text-slate-500">Aktivitas Harian</p>
</div>
<div class="p-2 bg-cyan-500/10 rounded-lg text-cyan-400">
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
></path>
</svg>
</div>
</div>
<!-- SVG Container -->
<div
class="relative h-64 w-full chart-container-bar flex items-end justify-between px-2 gap-2 md:gap-4"
>
<!-- Bar Items -->
<div
class="bar-item w-full bg-cyan-500/20 rounded-t-lg relative group/bar h-0"
data-height="40%"
>
<div
class="absolute bottom-0 w-full bg-cyan-500 rounded-t-lg transition-all duration-300 group-hover/bar:bg-cyan-400 h-full"
></div>
<div
class="absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-cyan-400 text-xs font-bold px-2 py-1 rounded opacity-0 group-hover/bar:opacity-100 transition-opacity z-20 pointer-events-none"
>
Mon
</div>
</div>
<div
class="bar-item w-full bg-cyan-500/20 rounded-t-lg relative group/bar h-0"
data-height="70%"
>
<div
class="absolute bottom-0 w-full bg-cyan-500 rounded-t-lg transition-all duration-300 group-hover/bar:bg-cyan-400 h-full"
></div>
<div
class="absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-cyan-400 text-xs font-bold px-2 py-1 rounded opacity-0 group-hover/bar:opacity-100 transition-opacity z-20 pointer-events-none"
>
Tue
</div>
</div>
<div
class="bar-item w-full bg-cyan-500/20 rounded-t-lg relative group/bar h-0"
data-height="55%"
>
<div
class="absolute bottom-0 w-full bg-cyan-500 rounded-t-lg transition-all duration-300 group-hover/bar:bg-cyan-400 h-full"
></div>
<div
class="absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-cyan-400 text-xs font-bold px-2 py-1 rounded opacity-0 group-hover/bar:opacity-100 transition-opacity z-20 pointer-events-none"
>
Wed
</div>
</div>
<div
class="bar-item w-full bg-cyan-500/20 rounded-t-lg relative group/bar h-0"
data-height="90%"
>
<div
class="absolute bottom-0 w-full bg-cyan-500 rounded-t-lg transition-all duration-300 group-hover/bar:bg-cyan-400 h-full shadow-[0_0_20px_rgba(6,182,212,0.5)]"
></div>
<div
class="absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-cyan-400 text-xs font-bold px-2 py-1 rounded opacity-0 group-hover/bar:opacity-100 transition-opacity z-20 pointer-events-none"
>
Thu
</div>
</div>
<div
class="bar-item w-full bg-cyan-500/20 rounded-t-lg relative group/bar h-0"
data-height="65%"
>
<div
class="absolute bottom-0 w-full bg-cyan-500 rounded-t-lg transition-all duration-300 group-hover/bar:bg-cyan-400 h-full"
></div>
<div
class="absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-cyan-400 text-xs font-bold px-2 py-1 rounded opacity-0 group-hover/bar:opacity-100 transition-opacity z-20 pointer-events-none"
>
Fri
</div>
</div>
<div
class="bar-item w-full bg-cyan-500/20 rounded-t-lg relative group/bar h-0"
data-height="80%"
>
<div
class="absolute bottom-0 w-full bg-cyan-500 rounded-t-lg transition-all duration-300 group-hover/bar:bg-cyan-400 h-full"
></div>
<div
class="absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-cyan-400 text-xs font-bold px-2 py-1 rounded opacity-0 group-hover/bar:opacity-100 transition-opacity z-20 pointer-events-none"
>
Sat
</div>
</div>
</div>
<!-- X-Axis Title -->
<div
class="absolute bottom-4 left-1/2 -translate-x-1/2 text-xs font-bold tracking-widest text-slate-500 uppercase mt-4"
>
Day of Week
</div>
</div>
</div>
</div>
</section>
<script>
gsap.registerPlugin(ScrollTrigger);
// --- LOGIC MORPHING CHART ---
// 1. LINE CHART ANIMATION
const chartContainerLine = document.querySelector(
".chart-container-line"
);
const morphingArea = document.querySelector(".morphing-area");
const morphingLine = document.querySelector(".morphing-line");
const dataPoints = document.querySelectorAll(".data-points circle");
// Set state awal: Data Points tidak terlihat & posisi acak di bawah
gsap.set(dataPoints, { autoAlpha: 0, y: 50 });
ScrollTrigger.create({
trigger: chartContainerLine,
start: "top 80%",
onEnter: () => {
// Animasi Area Fill (Dari datar ke bergelombang)
gsap.to(morphingArea, {
attr: { d: morphingArea.getAttribute("data-d-to") },
duration: 2,
ease: "power3.out",
});
// Animasi Garis (Line)
gsap.to(morphingLine, {
attr: { d: morphingLine.getAttribute("data-d-to") },
duration: 2,
ease: "power3.out",
});
// Animasi Titik Data (Muncul satu per satu)
gsap.to(dataPoints, {
autoAlpha: 1,
y: 0,
duration: 0.5,
stagger: 0.1,
ease: "back.out(2)",
delay: 0.5,
});
},
once: true,
});
// 2. BAR CHART ANIMATION
const chartContainerBar = document.querySelector(".chart-container-bar");
const bars = document.querySelectorAll(".bar-item");
ScrollTrigger.create({
trigger: chartContainerBar,
start: "top 80%",
onEnter: () => {
// Animate height dari 0 ke target
gsap.to(bars, {
height: (i, target) => target.getAttribute("data-height"),
duration: 1.5,
stagger: 0.1,
ease: "elastic.out(1, 0.5)",
});
},
once: true,
});
</script>
</body>
</html>
Penjelasan:
Langkah ini adalah inti visualisasi data yang paling kompleks namun menarik.
- Struktur: Dua grafik berbeda (Line/Area Chart untuk Revenue, Bar Chart untuk Engagement) dalam grid responsif.
- Morphing Logic (Area Chart):
- Kita mendefinisikan 2 path SVG:
d(bentuk awal/datar) dandata-d-to(bentuk akhir/bergelombang). - GSAP menginterpolasi atribut
ddari bentuk awal ke bentuk akhir secara halus. - Teknik ini diterapkan pada dua layer: Stroke (garis solid) dan Fill (gradien area di bawahnya) secara bersamaan.
- Kita mendefinisikan 2 path SVG:
- Elastic Logic (Bar Chart):
- Setiap bar dimulai dengan
height: 0atauheight: 0%. - GSAP menganimasikan
heightke target data yang ditentukan di atributdata-height. - Efek
elastic.outmembuat bar "membal" saat mencapai puncak, memberikan kesan organik.
- Setiap bar dimulai dengan
Langkah 4: Interactive Data Cards dengan Mouse Movement

Kartu data yang responsif terhadap pergerakan mouse menciptakan interaktivitas yang menarik.
File: datacards.html
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Langkah 4: Interactive 3D Cards</title>
<script src="<https://cdn.tailwindcss.com>"></script>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
<style>
@import url("<https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap>");
body { font-family: "Inter", sans-serif; }
/* Penting untuk efek 3D */
.perspective-container {
perspective: 1000px;
}
</style>
</head>
<body class="bg-[#020617] text-white overflow-x-hidden selection:bg-indigo-500/30">
<!-- INTERACTIVE CARDS SECTION -->
<section class="relative min-h-screen flex items-center justify-center py-20 px-6">
<!-- Background Decoration -->
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-4xl h-full max-h-[500px] bg-indigo-500/5 rounded-full blur-[120px] pointer-events-none"></div>
<div class="container mx-auto max-w-6xl relative z-10 perspective-container">
<div class="mb-16 text-center">
<h2 class="text-4xl md:text-5xl font-black mb-4 text-white">
Key <span class="text-emerald-400">Insights</span>
</h2>
<p class="text-slate-400 text-lg">
Arahkan kursor Anda ke kartu untuk melihat interaksi detail.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- Card 1 (Indigo) -->
<div class="interactive-card bg-slate-900/80 border border-white/10 rounded-3xl p-8 relative overflow-hidden group cursor-pointer backdrop-blur-xl h-[350px] flex flex-col justify-between transition-colors hover:border-indigo-500/50">
<!-- Spotlight Gradient (Initially hidden) -->
<div class="spotlight absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"
style="background: radial-gradient(600px circle at var(--mouse-x) var(--mouse-y), rgba(99,102,241,0.15), transparent 40%);">
</div>
<div class="relative z-10">
<div class="w-14 h-14 rounded-2xl bg-indigo-500/20 flex items-center justify-center text-indigo-400 mb-6">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>
</div>
<div class="text-5xl font-black text-white mb-2">+34%</div>
<h3 class="text-xl font-bold text-indigo-200 mb-4">Revenue Growth</h3>
<p class="text-slate-400 text-sm leading-relaxed">
Pertumbuhan konsisten selama 3 kuartal berturut-turut dengan proyeksi stabil untuk tahun depan.
</p>
</div>
</div>
<!-- Card 2 (Cyan) -->
<div class="interactive-card bg-slate-900/80 border border-white/10 rounded-3xl p-8 relative overflow-hidden group cursor-pointer backdrop-blur-xl h-[350px] flex flex-col justify-between transition-colors hover:border-cyan-500/50">
<div class="spotlight absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"
style="background: radial-gradient(600px circle at var(--mouse-x) var(--mouse-y), rgba(6,182,212,0.15), transparent 40%);">
</div>
<div class="relative z-10">
<div class="w-14 h-14 rounded-2xl bg-cyan-500/20 flex items-center justify-center text-cyan-400 mb-6">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
</div>
<div class="text-5xl font-black text-white mb-2">2.5M</div>
<h3 class="text-xl font-bold text-cyan-200 mb-4">Active Users</h3>
<p class="text-slate-400 text-sm leading-relaxed">
Basis pengguna aktif terus berkembang dengan retention rate 82% per bulan.
</p>
</div>
</div>
<!-- Card 3 (Emerald) -->
<div class="interactive-card bg-slate-900/80 border border-white/10 rounded-3xl p-8 relative overflow-hidden group cursor-pointer backdrop-blur-xl h-[350px] flex flex-col justify-between transition-colors hover:border-emerald-500/50">
<div class="spotlight absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"
style="background: radial-gradient(600px circle at var(--mouse-x) var(--mouse-y), rgba(16,185,129,0.15), transparent 40%);">
</div>
<div class="relative z-10">
<div class="w-14 h-14 rounded-2xl bg-emerald-500/20 flex items-center justify-center text-emerald-400 mb-6">
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<div class="text-5xl font-black text-white mb-2">99.9%</div>
<h3 class="text-xl font-bold text-emerald-200 mb-4">System Uptime</h3>
<p class="text-slate-400 text-sm leading-relaxed">
Infrastruktur cloud kami menjamin ketersediaan maksimal dengan monitoring 24/7.
</p>
</div>
</div>
</div>
</div>
</section>
<script>
// --- LOGIC INTERACTIVE CARDS ---
const cards = document.querySelectorAll(".interactive-card");
cards.forEach((card) => {
card.addEventListener("mousemove", (e) => {
const rect = card.getBoundingClientRect();
// Hitung posisi mouse relatif terhadap kartu (0,0 di kiri atas kartu)
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Update CSS variables untuk gradient spotlight
card.style.setProperty("--mouse-x", `${x}px`);
card.style.setProperty("--mouse-y", `${y}px`);
// Hitung rotasi 3D (Tilt Effect)
// Range: -10deg sampai 10deg
const xCenter = rect.width / 2;
const yCenter = rect.height / 2;
const rotateY = ((x - xCenter) / xCenter) * 10; // Kiri-Kanan
const rotateX = ((y - yCenter) / yCenter) * -10; // Atas-Bawah (Inverted)
gsap.to(card, {
rotationY: rotateY,
rotationX: rotateX,
scale: 1.02, // Sedikit zoom
transformPerspective: 1000,
duration: 0.4,
ease: "power2.out"
});
});
// Reset saat mouse keluar
card.addEventListener("mouseleave", () => {
gsap.to(card, {
rotationY: 0,
rotationX: 0,
scale: 1,
duration: 0.7,
ease: "elastic.out(1, 0.5)" // Efek membal saat reset
});
});
});
</script>
</body>
</html>
Penjelasan:
Langkah ini menambahkan elemen interaksi pengguna yang menyenangkan (micro-interaction).
- Konsep "Spotlight": Kartu akan merespons posisi kursor mouse pengguna.
- Visual Effect:
- Gradient Follow: Sebuah gradient radial redup akan bergerak mengikuti kursor di atas kartu (
background: radial-gradient(...)). - 3D Tilt: Kartu akan sedikit miring (rotasi 3D) mengikuti arah mouse, memberikan efek kedalaman.
- Gradient Follow: Sebuah gradient radial redup akan bergerak mengikuti kursor di atas kartu (
- Logika JS:
- Event listener
mousemovemenghitung posisi mouse relatif terhadap kartu (e.clientX - rect.left). - GSAP digunakan untuk mengubah rotasi (
rotationX,rotationY) secara halus. - Event
mouseleavemengembalikan kartu ke posisi semula.
- Event listener
Langkah 5: Number Formatter & Currency
Penting untuk memformat angka dengan benar agar mudah dibaca.

file: formatter.html
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Langkah 5: Smart Number Formatting</title>
<script src="<https://cdn.tailwindcss.com>"></script>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js>"></script>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js>"></script>
<style>
@import url("<https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap>");
body {
font-family: "Inter", sans-serif;
}
/* Grid Background Pattern (Konsisten dengan Langkah 1) */
.bg-grid {
background-size: 40px 40px;
background-image: linear-gradient(
to right,
rgba(255, 255, 255, 0.03) 1px,
transparent 1px
),
linear-gradient(
to bottom,
rgba(255, 255, 255, 0.03) 1px,
transparent 1px
);
}
</style>
</head>
<body
class="bg-[#020617] text-white overflow-x-hidden selection:bg-indigo-500/30"
>
<!-- SECTION CONTAINER -->
<section
class="relative min-h-screen flex items-center justify-center py-20 px-6"
>
<!-- Background Elements -->
<div class="absolute inset-0 -z-10 overflow-hidden">
<div class="absolute inset-0 bg-grid"></div>
<div
class="absolute top-0 right-0 w-[600px] h-[600px] bg-indigo-600/10 rounded-full blur-[120px] pointer-events-none"
></div>
<div
class="absolute bottom-0 left-0 w-[500px] h-[500px] bg-cyan-600/10 rounded-full blur-[120px] pointer-events-none"
></div>
</div>
<div class="container mx-auto max-w-5xl relative z-10">
<!-- Header -->
<div class="text-center mb-16">
<div
class="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-indigo-500/30 bg-indigo-500/10 text-indigo-300 text-xs font-medium mb-6"
>
<span>💎</span> Premium Data Formatting
</div>
<h2 class="text-4xl md:text-5xl font-black mb-4 text-white">
Financial
<span
class="text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 to-cyan-400"
>Overview</span
>
</h2>
<p class="text-slate-400 text-lg max-w-2xl mx-auto">
Teknik memformat raw data menjadi informasi yang mudah dibaca
(Currency, Percent, & Compact) secara real-time.
</p>
</div>
<!-- Cards Grid -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- Card 1: CURRENCY (Net Profit) -->
<div
class="group bg-slate-900/60 border border-white/10 rounded-3xl p-8 backdrop-blur-xl hover:border-indigo-500/50 transition-all duration-500 hover:-translate-y-1"
>
<div class="flex items-center justify-between mb-6">
<div
class="w-12 h-12 rounded-2xl bg-indigo-500/20 flex items-center justify-center text-indigo-400 group-hover:scale-110 transition-transform duration-500"
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</div>
<span
class="text-xs font-bold tracking-wider text-slate-500 uppercase"
>Yearly</span
>
</div>
<div class="space-y-1">
<p class="text-slate-400 text-sm font-medium">Net Profit</p>
<div
class="text-3xl xl:text-4xl font-black text-white tracking-tight truncate counter"
data-target="2540300"
data-type="currency"
title="$2,540,300"
>
$0
</div>
</div>
<div
class="mt-6 pt-6 border-t border-white/5 flex items-center gap-2 text-sm text-emerald-400"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
></path>
</svg>
<span>+18.2% vs target</span>
</div>
</div>
<!-- Card 2: PERCENTAGE (Growth) -->
<div
class="group bg-slate-900/60 border border-white/10 rounded-3xl p-8 backdrop-blur-xl hover:border-cyan-500/50 transition-all duration-500 hover:-translate-y-1"
>
<div class="flex items-center justify-between mb-6">
<div
class="w-12 h-12 rounded-2xl bg-cyan-500/20 flex items-center justify-center text-cyan-400 group-hover:scale-110 transition-transform duration-500"
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
></path>
</svg>
</div>
<span
class="text-xs font-bold tracking-wider text-slate-500 uppercase"
>Q4 2025</span
>
</div>
<div class="space-y-1">
<p class="text-slate-400 text-sm font-medium">Growth Rate</p>
<div
class="text-4xl md:text-5xl font-black text-white tracking-tight counter"
data-target="84.5"
data-type="percent"
>
0%
</div>
</div>
<div
class="mt-6 pt-6 border-t border-white/5 flex items-center gap-2 text-sm text-cyan-400"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>Top 5% Industry</span>
</div>
</div>
<!-- Card 3: COMPACT (Total Transactions) -->
<div
class="group bg-slate-900/60 border border-white/10 rounded-3xl p-8 backdrop-blur-xl hover:border-emerald-500/50 transition-all duration-500 hover:-translate-y-1"
>
<div class="flex items-center justify-between mb-6">
<div
class="w-12 h-12 rounded-2xl bg-emerald-500/20 flex items-center justify-center text-emerald-400 group-hover:scale-110 transition-transform duration-500"
>
<svg
class="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
></path>
</svg>
</div>
<span
class="text-xs font-bold tracking-wider text-slate-500 uppercase"
>Lifetime</span
>
</div>
<div class="space-y-1">
<p class="text-slate-400 text-sm font-medium">Transactions</p>
<!-- Data Target: 1.45 Juta -->
<div
class="text-4xl md:text-5xl font-black text-white tracking-tight counter"
data-target="1450000"
data-type="compact"
>
0
</div>
</div>
<div
class="mt-6 pt-6 border-t border-white/5 flex items-center gap-2 text-sm text-slate-400"
>
<span>Volume tinggi</span>
<span
class="px-2 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-[10px] font-bold"
>HIGH VOL</span
>
</div>
</div>
</div>
<!-- Replay Button -->
<div class="mt-16 text-center">
<button
onclick="runAnimations()"
class="group relative px-8 py-3 rounded-full bg-white/5 border border-white/10 text-sm font-semibold hover:bg-white/10 transition-all hover:scale-105 active:scale-95"
>
<span class="relative z-10 flex items-center gap-2">
<svg
class="w-4 h-4 text-indigo-400 group-hover:rotate-180 transition-transform duration-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
></path>
</svg>
Replay Animation
</span>
</button>
</div>
</div>
</section>
<script>
gsap.registerPlugin(ScrollTrigger);
// --- UTILITY: ADVANCED FORMATTER ---
function formatNumber(value, type) {
switch (type) {
case "currency":
// Format Currency: $2,540,300
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(value);
case "percent":
// Format Percent: 84.5% (Fixed 1 decimal)
return value.toFixed(1) + "%";
case "compact":
// Format Compact: 1.4M (Short notation)
return new Intl.NumberFormat("en-US", {
notation: "compact",
compactDisplay: "short",
maximumFractionDigits: 1,
}).format(value);
default:
return Math.floor(value).toLocaleString("en-US");
}
}
// --- ANIMATION LOGIC ---
function runAnimations() {
const counters = document.querySelectorAll(".counter");
counters.forEach((counter) => {
const target = parseFloat(counter.getAttribute("data-target"));
const type = counter.getAttribute("data-type");
// Object proxy untuk animasi nilai mentah
const proxy = { val: 0 };
gsap.to(proxy, {
val: target,
duration: 2.5,
ease: "power3.out", // Easing yang smooth (cepat di awal, pelan di akhir)
onUpdate: function () {
// Update textContent dengan nilai yang sudah diformat
counter.textContent = formatNumber(this.targets()[0].val, type);
},
});
});
}
// Start animation on load
runAnimations();
</script>
</body>
</html>
Penjelasan:
Langkah ini fokus pada utilitas (utility) untuk memformat angka mentah menjadi format yang ramah pengguna (misal: 2500000 menjadi $2,500,000 atau 2.5M).
- Utility Function: Kita membuat fungsi
formatNumberyang fleksibel menangani tipecurrency,percent, dancompact(singkatan K/M/B). - Implementasi: Kita menerapkan fungsi ini pada animasi GSAP
onUpdateagar angka yang sedang berjalan tetap terformat rapi selama animasi berlangsung.
Dengan menggabungkan 5 teknik di atas, kita telah menciptakan dashboard yang tidak hanya informatif tetapi juga menyenangkan untuk dilihat dan diinteraksikan. Kunci kesuksesan adalah:
- Timing yang tepat: Tidak semua animasi harus bersamaan.
- Easing yang smooth: Gunakan
power2.out,back.out, dll. sesuai konteks. - ScrollTrigger untuk context: Animasi hanya berjalan saat user scroll ke section tersebut.
- Interactive element: Responsivitas terhadap mouse membuat experience lebih engaging.
Data visualization dengan GSAP adalah seni yang menggabungkan storytelling, animasi, dan fungsi. Semoga tutorial ini membantu Anda membuat dashboard yang memukau!