Component

Goal Progress Tracker

Fundraising goal tracker card with an animated gradient progress bar, milestone markers, live stats, and quick-amount sponsor actions. Reaching the goal flips the card into a funded success state.

  • Component
  • Tailwind
  • HTML
  • MIT

Preview

Live component

Interactive preview
Open preview
9:41 100%
devsnips.dev/library/tailwind/components/progress/goal-progress/
fluid · no fixed width 100%
No dependencies Reduced-motion

Install

Add to your project

terminal
npx devsnips add Tailwind/Components/Progress/goal-progress
registry path: Tailwind/Components/Progress/goal-progress

Source

Implementation

code.html
<!--
Snippet Name: Goal Progress Tracker
Description: Fundraising goal card with an animated progress bar, milestone markers, live stats, and quick-amount sponsor actions. Reaching the goal flips the card into a funded success state.
Author: DevSnips Contributors
Usage Example: Drop into a sponsorship, crowdfunding, or savings-goal page. Adjust the goal/raised/backers values in the script to match your data.
-->
<div class="w-full max-w-2xl" data-goal-progress="goal-progress">
  <div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-slate-200/60 sm:p-8">

    <div class="flex items-start justify-between gap-4">
      <div class="min-w-0">
        <p class="text-xs font-semibold uppercase tracking-widest text-indigo-600">Community goal</p>
        <h3 class="mt-2 text-xl font-bold tracking-tight text-slate-900 sm:text-2xl">Fund the next DevSnips release</h3>
        <p class="mt-2 text-sm leading-relaxed text-slate-600">Every pledge goes straight into maintainer time for v2.0 — new React families, a public Figma kit, and full RTL coverage.</p>
      </div>
      <span class="gp-pill inline-flex shrink-0 items-center gap-1.5 rounded-full bg-indigo-50 px-3 py-1.5 text-sm font-semibold tabular-nums text-indigo-700">74%</span>
    </div>

    <div class="mt-6 flex flex-wrap items-baseline gap-x-2 gap-y-1">
      <span class="gp-raised text-3xl font-bold tracking-tight tabular-nums text-slate-900">$18,450</span>
      <span class="text-sm text-slate-500">raised of <span class="font-semibold text-slate-700 tabular-nums">$25,000</span> goal</span>
    </div>

    <div class="relative mt-5">
      <div
        class="h-2.5 w-full overflow-hidden rounded-full bg-slate-100"
        role="progressbar"
        aria-label="Sponsorship goal progress"
        aria-valuemin="0"
        aria-valuemax="25000"
        aria-valuenow="18450"
        aria-valuetext="$18,450 of $25,000 raised (74%)"
      >
        <div class="gp-fill h-full w-0 rounded-full bg-gradient-to-r from-indigo-500 to-violet-500 motion-safe:transition-[width] motion-safe:duration-500 motion-safe:ease-out" style="width: 74%"></div>
      </div>
      <span class="gp-dot absolute top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-indigo-500 ring-2 ring-white" style="left: 25%" data-threshold="25" aria-hidden="true"></span>
      <span class="gp-dot absolute top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-indigo-500 ring-2 ring-white" style="left: 50%" data-threshold="50" aria-hidden="true"></span>
      <span class="gp-dot absolute top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-slate-300 ring-2 ring-white" style="left: 75%" data-threshold="75" aria-hidden="true"></span>
      <span class="gp-dot absolute top-1/2 h-3.5 w-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-slate-300 ring-2 ring-white" style="left: 100%" data-threshold="100" aria-hidden="true"></span>
    </div>

    <div class="relative mt-3 h-4 text-[11px] font-medium tabular-nums">
      <span class="gp-dot-label absolute top-0 -translate-x-1/2 text-indigo-600" style="left: 25%" data-threshold="25">$6.25k</span>
      <span class="gp-dot-label absolute top-0 -translate-x-1/2 text-indigo-600" style="left: 50%" data-threshold="50">$12.5k</span>
      <span class="gp-dot-label absolute top-0 -translate-x-1/2 text-slate-500" style="left: 75%" data-threshold="75">$18.75k</span>
      <span class="gp-dot-label absolute right-0 top-0 text-slate-500" data-threshold="100">$25k</span>
    </div>

    <dl class="mt-6 grid grid-cols-3 divide-x divide-slate-100 rounded-2xl bg-slate-50 py-4 text-center ring-1 ring-slate-200/60">
      <div class="px-2">
        <dt class="text-xs font-medium uppercase tracking-wider text-slate-500">Backers</dt>
        <dd class="gp-backers mt-1 text-lg font-bold tabular-nums text-slate-900">214</dd>
      </div>
      <div class="px-2">
        <dt class="text-xs font-medium uppercase tracking-wider text-slate-500">Days left</dt>
        <dd class="mt-1 text-lg font-bold tabular-nums text-slate-900">12</dd>
      </div>
      <div class="px-2">
        <dt class="text-xs font-medium uppercase tracking-wider text-slate-500">Avg. pledge</dt>
        <dd class="gp-avg mt-1 text-lg font-bold tabular-nums text-slate-900">$86</dd>
      </div>
    </dl>

    <div class="mt-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
      <div class="flex items-center gap-2" role="group" aria-label="Choose a pledge amount">
        <button type="button" class="gp-chip inline-flex h-11 items-center justify-center rounded-full bg-white px-5 text-sm font-semibold text-slate-700 ring-1 ring-slate-200 transition-colors duration-200 hover:text-indigo-700 hover:ring-indigo-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" data-amount="10" aria-pressed="false">$10</button>
        <button type="button" class="gp-chip inline-flex h-11 items-center justify-center rounded-full bg-indigo-600 px-5 text-sm font-semibold text-white ring-1 ring-indigo-600 transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" data-amount="25" aria-pressed="true">$25</button>
        <button type="button" class="gp-chip inline-flex h-11 items-center justify-center rounded-full bg-white px-5 text-sm font-semibold text-slate-700 ring-1 ring-slate-200 transition-colors duration-200 hover:text-indigo-700 hover:ring-indigo-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" data-amount="50" aria-pressed="false">$50</button>
      </div>
      <button type="button" class="gp-sponsor inline-flex h-11 items-center justify-center gap-2 rounded-full bg-indigo-600 px-6 text-sm font-semibold text-white shadow-sm transition-all duration-200 ease-out hover:bg-indigo-700 motion-safe:hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2 motion-safe:active:translate-y-0 disabled:cursor-not-allowed disabled:opacity-50">
        <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"/></svg>
        <span class="gp-sponsor-label">Sponsor $25</span>
      </button>
    </div>

    <p class="gp-status mt-4 flex items-center gap-2 text-sm text-slate-600" aria-live="polite">
      <svg class="h-4 w-4 shrink-0 text-indigo-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
      <span class="gp-status-text">$6,550 to go · 12 days left.</span>
    </p>

  </div>
</div>

<script>
  (function () {
    const root = document.currentScript.closest('[data-goal-progress="goal-progress"]');
    if (!root) return;

    const GOAL = 25000;
    let raised = 18450;
    let backers = 214;
    let selected = 25;

    const fill = root.querySelector('.gp-fill');
    const bar = root.querySelector('[role="progressbar"]');
    const pill = root.querySelector('.gp-pill');
    const raisedEl = root.querySelector('.gp-raised');
    const backersEl = root.querySelector('.gp-backers');
    const avgEl = root.querySelector('.gp-avg');
    const statusText = root.querySelector('.gp-status-text');
    const sponsor = root.querySelector('.gp-sponsor');
    const sponsorLabel = root.querySelector('.gp-sponsor-label');
    const chips = Array.from(root.querySelectorAll('.gp-chip'));
    const dots = Array.from(root.querySelectorAll('.gp-dot'));
    const dotLabels = Array.from(root.querySelectorAll('.gp-dot-label'));

    const CHIP_ON = ['bg-indigo-600', 'text-white', 'ring-indigo-600'];
    const CHIP_OFF = ['bg-white', 'text-slate-700', 'ring-slate-200', 'hover:text-indigo-700', 'hover:ring-indigo-300'];

    function money(n) { return '$' + n.toLocaleString('en-US'); }

    function render() {
      const pct = Math.min(100, Math.round((raised / GOAL) * 100));
      const funded = raised >= GOAL;

      fill.style.width = pct + '%';
      fill.classList.toggle('from-indigo-500', !funded);
      fill.classList.toggle('to-violet-500', !funded);
      fill.classList.toggle('from-emerald-500', funded);
      fill.classList.toggle('to-teal-400', funded);
      bar.setAttribute('aria-valuenow', String(raised));
      bar.setAttribute('aria-valuetext', money(raised) + ' of ' + money(GOAL) + ' raised (' + pct + '%)');

      if (funded) {
        pill.className = 'gp-pill inline-flex shrink-0 items-center gap-1.5 rounded-full bg-emerald-50 px-3 py-1.5 text-sm font-semibold text-emerald-700';
        pill.innerHTML = '<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>Funded';
        sponsor.disabled = true;
        chips.forEach(function (c) { c.disabled = true; });
        sponsorLabel.textContent = 'Goal reached';
        statusText.textContent = 'Goal reached — thank you to all ' + backers + ' backers!';
      } else {
        pill.textContent = pct + '%';
        sponsorLabel.textContent = 'Sponsor ' + money(selected);
        statusText.textContent = money(GOAL - raised) + ' to go · 12 days left.';
      }

      raisedEl.textContent = money(raised);
      backersEl.textContent = backers.toLocaleString('en-US');
      avgEl.textContent = money(Math.round(raised / backers));

      dots.forEach(function (dot) {
        const hit = pct >= Number(dot.dataset.threshold);
        dot.classList.toggle('bg-indigo-500', hit && !funded);
        dot.classList.toggle('bg-emerald-500', hit && funded);
        dot.classList.toggle('bg-slate-300', !hit);
      });
      dotLabels.forEach(function (label) {
        const hit = pct >= Number(label.dataset.threshold);
        label.classList.toggle('text-indigo-600', hit && !funded);
        label.classList.toggle('text-emerald-600', hit && funded);
        label.classList.toggle('text-slate-500', !hit);
      });
    }

    chips.forEach(function (chip) {
      chip.addEventListener('click', function () {
        selected = Number(chip.dataset.amount);
        chips.forEach(function (c) {
          const on = c === chip;
          c.setAttribute('aria-pressed', String(on));
          CHIP_ON.forEach(function (cls) { c.classList.toggle(cls, on); });
          CHIP_OFF.forEach(function (cls) { c.classList.toggle(cls, !on); });
        });
        sponsorLabel.textContent = 'Sponsor ' + money(selected);
      });
    });

    sponsor.addEventListener('click', function () {
      if (raised >= GOAL) return;
      raised = Math.min(GOAL, raised + selected);
      backers += 1;
      render();
    });

    render();
  })();
</script>
177 lines UTF-8 · LF · Spaces: 2

AI-ready

Available to your agent.

This component is reachable through the DevSnips CLI, MCP server and Skills integrations — one registry, one resource path.

Resource path

devsnips://components/progress/goal-progress

Continue browsing

View all