webdevpuneet.com

Metric KPI Card Grid — Animated Dashboard Cards HTML CSS JS

Metric KPI Card Grid · Dashboards · Plain HTML, CSS & JS · Live preview

CategoryDashboards

Copy & paste

Source Code

<div class="page">
  <header class="page-header">
    <div>
      <h1 class="page-title">Overview</h1>
      <p class="page-sub"><span class="live-dot" aria-hidden="true"></span><span id="periodLabel">Last 30 days</span> · Updated just now</p>
    </div>
    <div class="period-toggle" role="group" aria-label="Time period" style="--i:0">
      <span class="pt-ind" aria-hidden="true"></span>
      <button class="pt-btn active" type="button" data-period="30d" aria-pressed="true">30d</button>
      <button class="pt-btn" type="button" data-period="7d" aria-pressed="false">7d</button>
      <button class="pt-btn" type="button" data-period="1d" aria-pressed="false">24h</button>
    </div>
  </header>

  <section class="metric-grid" aria-label="Key metrics">
    <article class="metric-card" data-key="users" style="--c:#6366f1;--ct:rgba(99,102,241,.14);--n:0">
      <div class="metric-top">
        <div class="metric-icon">
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
        </div>
        <span class="trend up"><span class="trend-arrow" aria-hidden="true">↑</span><span class="trend-val">12.5%</span></span>
      </div>
      <div class="metric-val">24,531</div>
      <div class="metric-label">Total Users</div>
      <div class="spark"></div>
    </article>

    <article class="metric-card" data-key="revenue" style="--c:#10b981;--ct:rgba(16,185,129,.14);--n:1">
      <div class="metric-top">
        <div class="metric-icon">
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
        </div>
        <span class="trend up"><span class="trend-arrow" aria-hidden="true">↑</span><span class="trend-val">8.2%</span></span>
      </div>
      <div class="metric-val">$48,290</div>
      <div class="metric-label">Monthly Revenue</div>
      <div class="spark"></div>
    </article>

    <article class="metric-card" data-key="session" style="--c:#f59e0b;--ct:rgba(245,158,11,.16);--n:2">
      <div class="metric-top">
        <div class="metric-icon">
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
        </div>
        <span class="trend down"><span class="trend-arrow" aria-hidden="true">↓</span><span class="trend-val">2.1%</span></span>
      </div>
      <div class="metric-val">3m 42s</div>
      <div class="metric-label">Avg. Session</div>
      <div class="spark"></div>
    </article>

    <article class="metric-card" data-key="conv" style="--c:#a855f7;--ct:rgba(168,85,247,.14);--n:3">
      <div class="metric-top">
        <div class="metric-icon">
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
        </div>
        <span class="trend up"><span class="trend-arrow" aria-hidden="true">↑</span><span class="trend-val">4.7%</span></span>
      </div>
      <div class="metric-val">73.4%</div>
      <div class="metric-label">Conversion Rate</div>
      <div class="spark"></div>
    </article>
  </section>

  <section class="breakdown-row">
    <div class="breakdown-card">
      <h3 class="breakdown-title">Top Channels</h3>
      <div class="channel-list">
        <div class="channel-item">
          <span class="ch-name"><i style="background:#6366f1"></i>Organic Search</span>
          <div class="ch-bar-wrap"><div class="ch-bar" style="background:#6366f1"></div></div>
          <span class="ch-pct">68%</span>
        </div>
        <div class="channel-item">
          <span class="ch-name"><i style="background:#10b981"></i>Direct</span>
          <div class="ch-bar-wrap"><div class="ch-bar" style="background:#10b981"></div></div>
          <span class="ch-pct">22%</span>
        </div>
        <div class="channel-item">
          <span class="ch-name"><i style="background:#f59e0b"></i>Social</span>
          <div class="ch-bar-wrap"><div class="ch-bar" style="background:#f59e0b"></div></div>
          <span class="ch-pct">10%</span>
        </div>
      </div>
    </div>
    <div class="breakdown-card">
      <h3 class="breakdown-title">Goal Progress</h3>
      <div class="goals">
        <div class="goal-item">
          <div class="goal-head"><span>Revenue target</span><span class="goal-pct">82%</span></div>
          <div class="goal-track"><div class="goal-fill" style="background:linear-gradient(90deg,#6366f1,#8b5cf6)"></div></div>
        </div>
        <div class="goal-item">
          <div class="goal-head"><span>User growth</span><span class="goal-pct">91%</span></div>
          <div class="goal-track"><div class="goal-fill" style="background:linear-gradient(90deg,#10b981,#059669)"></div></div>
        </div>
        <div class="goal-item">
          <div class="goal-head"><span>Churn reduction</span><span class="goal-pct">44%</span></div>
          <div class="goal-track"><div class="goal-fill" style="background:linear-gradient(90deg,#f59e0b,#ef4444)"></div></div>
        </div>
      </div>
    </div>
  </section>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Smooth SVG area sparklines
Catmull-Rom splines drawn as an SVG path with a gradient fill that scales to any card width and redraws with a left-to-right wipe animation on every period change.
Hover and touch tooltips
One pointer handler snaps a dot, crosshair and value tooltip to the nearest data point — mouse, pen and touch, with no chart library.
Count-up numbers
Values animate from their previous number to the new one with an ease-out curve, using locale formatting, currency and mm:ss time formatters.
Working period toggle
A segmented control with a sliding pill (one CSS custom property) that really re-renders cards, sparklines and breakdown bars from the data object.
Good/bad-aware trend badges
Arrow + percentage + tinted pill, with a lowerIsBetter flag so a falling churn rate is green and a falling revenue is red. Includes an aria-label.
Animated breakdown bars
Channel share and goal progress bars transition to new widths while their percentages count up, so a period change feels continuous.
Light and dark mode
Every colour is a CSS variable with a prefers-color-scheme: dark override, so the dashboard follows the visitor's system theme automatically.
Cursor spotlight and stagger
Cards rise in one after another and show a soft accent glow that follows the pointer. Reduced-motion users get final values with no animation.

About this UI Snippet

Metric KPI Card Grid — Smooth Sparklines, Count-Up Numbers & Dashboard Layout Pattern

Screenshot of the Metric KPI Card Grid snippet rendered live

KPI metric cards are the centrepiece of every analytics dashboard — they surface the numbers that matter most and show direction (up, down, flat) at a glance. This snippet builds a complete dashboard overview: four metric cards with count-up values, smooth SVG area sparklines you can hover for exact values, good/bad-aware trend badges, a sliding period toggle that really swaps the data, and two breakdown panels (channel share and goal progress) whose bars animate to their new widths. It follows the system light/dark preference with no extra code.

The design challenge is information density. Each card has to carry a value, a direction, the size of the change and a trend over time without feeling crowded. The icon tile, trend pill and sparkline each add one signal layer, and the big tabular-number value stays the loudest thing on the card.

Smooth sparklines from one data array

Every card's sparkline is generated from a plain array in the PERIODS object. The script normalises the points between that series' own min and max, converts them to a Catmull-Rom spline (a series of cubic Bézier curves) and draws it as an SVG path with a soft gradient area underneath. The SVG uses preserveAspectRatio="none" so it stretches to any card width, and vector-effect: non-scaling-stroke keeps the line a constant 2px instead of getting squashed. On every redraw the chart wipes in from left to right with an animated clip-path.

Hover, touch and tooltips without a library

A single pointermove handler maps the cursor's x position to the nearest data index, then moves an HTML dot, a crosshair line and a tooltip to that point. The dot and tooltip are normal elements positioned by percentage, so they stay perfectly round even though the SVG underneath is stretched. Because it uses pointer events, the same code works for mouse, pen and touch.

Count-up values and a working period toggle

Switching 30d / 7d / 24h doesn't just move a highlight — render() reads the new period's data, counts every number from its previous value to the new one with requestAnimationFrame and an ease-out curve, redraws the sparklines and animates the breakdown bars to their new widths. The active pill is a single absolutely positioned element that slides using a --i custom property, so the motion is one CSS transform — the same idea as the segmented control, and the count-up follows the number ticker approach.

Trend badges that know good from bad

A trend badge combines an arrow, the percentage and a tinted background, so direction never relies on colour alone, and it carries an aria-label for screen readers. Direction and meaning are separate: the badge class is up/down from the sign, then good/bad from the metric's lowerIsBetter flag. A falling churn rate or page load time can therefore show a down arrow in green, while a falling session length shows a down arrow in red.

Theming and motion

All colours are CSS variables defined on :root with a prefers-color-scheme: dark override, and each card receives its accent through --c and --ct. Cards fade up with a staggered delay, a soft spotlight follows the cursor, and prefers-reduced-motion switches the animations off and shows final values immediately.

Need a bigger chart than a sparkline? Pair the grid with an area chart or a standalone sparkline chart, show goal health with a donut progress ring, or build a full draggable layout from the dashboard widget grid.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSFour KPI cards fade in with counting values and wipe-in sparklines. Below them sit a channel breakdown and goal progress panel whose bars animate to their values.
  2. 2
    Read the metric cardsTotal Users (↑12.5%), Monthly Revenue (↑8.2%), Avg. Session (↓2.1%, shown red because shorter sessions are bad) and Conversion Rate (↑4.7%). Each card includes a spline sparkline of the period's trend.
  3. 3
    Hover or touch a sparklineA dot and crosshair snap to the nearest data point and a tooltip shows its label (Day 8, Hour 12…) and value. It works the same with a finger drag on touch screens.
  4. 4
    Switch the periodClick 30d, 7d or 24h. The pill slides across, every value counts to its new number, sparklines redraw and the breakdown bars animate to new widths — all from the PERIODS object.
  5. 5
    Replace the dataEdit the PERIODS object: v is the value, t the signed percentage change, d the sparkline points. For an API, fill PERIODS from your response and call render(period).
  6. 6
    Mark "lower is better" metricsFor churn, errors or latency, add lowerIsBetter: true to that metric in METRICS. A falling number then gets a green badge instead of red.

Real-world uses

Common Use Cases

SaaS analytics dashboards
The primary use case — user growth, revenue, activation and churn in the main overview. The period toggle drives the date range and every number animates to its new value.
E-commerce performance dashboards
Orders, revenue, conversion rate and average order value as headline metrics, with the channel panel showing where traffic comes from.
Marketing analytics overviews
Impressions, clicks, conversions and CPA as KPIs. Mark CPA as lowerIsBetter and use goal bars to show campaign pacing against monthly targets.
Executive reporting dashboards
High-level business numbers for leadership — a value, a direction and a trend line per metric, with no raw data tables needed.
DevOps monitoring dashboards
Requests per second, error rate, p95 latency and uptime. Hover the sparkline to read the exact hourly value; error rate and latency use lowerIsBetter.
Internal team productivity tools
Tickets closed, velocity, deployment frequency and cycle time — the DORA metrics overview, with a 7-day and 30-day view.
Related: Area Chart
Put a full area chart under the KPI cards for the detailed trend, and a dashboard widget grid around both for a draggable layout.

Got questions?

Frequently Asked Questions

The toggle already calls render(period), which reads from the PERIODS object. Fetch your metrics, fill PERIODS[period] in the same shape (value v, change t, points d), then call render. Cancel or ignore stale responses if users switch periods quickly.

Add lowerIsBetter: true to that metric inside METRICS, for example churn: { fmt: ..., lowerIsBetter: true }. The arrow still points down, but the badge gets the green good style instead of red.

Copy one <article class="metric-card" data-key="...">, give it a new data-key and accent colours, add a matching entry to METRICS and to each period in PERIODS. Then adjust the grid to repeat(auto-fit, minmax(190px, 1fr)) if you want it to wrap on its own.

A spline area line shows trend more clearly at small sizes and enables a snap-to-point hover. It uses one path per card, so it stays light. If you prefer bars, replace drawSpark with a function that creates one <div> per value sized by value / max.

Make a MetricCard with {label, value, change, data, color, icon, lowerIsBetter} props and a Sparkline that renders the SVG path from data. Keep the active period in useState, fetch in useEffect([period]), and reuse the count-up logic in a small useCountUp hook.