Component

Usage Meter

Dark, minimal plan-usage panel with four accessible quota meters (bandwidth, invocations, image optimization, build minutes). Meters hydrate from data-used/data-limit attributes, animate to their values, and flag near-limit state in text — ideal for billing and account settings pages.

  • Component
  • Tailwind
  • HTML
  • MIT

Preview

Live component

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

Install

Add to your project

terminal
npx devsnips add Tailwind/Components/Progress/usage-meter
registry path: Tailwind/Components/Progress/usage-meter

Source

Implementation

code.html
<!--
Snippet Name: Usage Meter
Description: Dark, minimal plan-usage panel with accessible quota meters. Each meter hydrates from data-used / data-limit attributes, animates to its value, and flags near-limit state in text.
Author: DevSnips Contributors
Usage Example: Drop into a billing or account settings page. Set data-used, data-limit, and data-unit on each [data-meter] row; the scoped script formats values, sets ARIA, and animates the fills.
-->
<section
  class="w-full max-w-2xl rounded-lg border border-white/10 bg-[#0a0a0a] p-6 text-white sm:p-8"
  data-usage-meter="usage-meter"
  aria-labelledby="usage-meter-heading"
>
  <header class="flex flex-wrap items-start justify-between gap-4">
    <div>
      <p class="text-[11px] font-medium uppercase tracking-widest text-white/50">Plan usage</p>
      <h2 id="usage-meter-heading" class="mt-1.5 text-lg font-medium tracking-tight text-white">
        Current billing cycle
      </h2>
      <p class="mt-1 text-xs text-white/50">
        Aug 1 – Aug 31, 2026 ·
        <span data-usage-updated>Updated 4 minutes ago</span>
      </p>
    </div>
    <div class="flex items-center gap-3">
      <span class="rounded-md bg-[#50e3c2]/10 px-2 py-1 text-[11px] font-medium uppercase tracking-widest text-[#50e3c2] ring-1 ring-inset ring-[#50e3c2]/30">
        Pro
      </span>
      <button
        type="button"
        data-usage-refresh
        aria-label="Refresh usage data"
        class="inline-flex h-8 w-8 items-center justify-center rounded-md text-white/50 transition-colors duration-150 hover:bg-white/5 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#50e3c2]"
      >
        <svg data-usage-refresh-icon 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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.582m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
        </svg>
      </button>
    </div>
  </header>

  <ul role="list" class="mt-6 divide-y divide-white/10 border-y border-white/10" aria-label="Usage by resource">
    <li class="py-4" data-meter data-name="Bandwidth" data-used="812" data-limit="1000" data-unit="GB">
      <div class="flex items-baseline justify-between gap-4">
        <p class="text-sm font-medium text-white">Bandwidth</p>
        <p class="text-sm tabular-nums text-white/50">
          <span data-meter-value class="text-white">812</span>&nbsp;of&nbsp;<span data-meter-limit>1,000</span>&nbsp;GB
          <span data-meter-percent class="ml-2 text-xs text-white/50">81%</span>
        </p>
      </div>
      <div
        class="mt-2.5 h-1 w-full overflow-hidden rounded-full bg-white/10"
        role="progressbar"
        aria-label="Bandwidth"
        aria-valuemin="0"
        aria-valuemax="1000"
        aria-valuenow="812"
        aria-valuetext="812 of 1,000 GB used, 81 percent"
      >
        <div data-meter-fill class="h-full w-[81%] rounded-full bg-[#50e3c2] transition-[width] duration-700 ease-out"></div>
      </div>
      <p data-meter-note class="mt-2 hidden items-center gap-1.5 text-xs font-medium text-white" aria-live="polite">
        <span class="h-1.5 w-1.5 rounded-full bg-[#50e3c2]" aria-hidden="true"></span>
        Approaching limit — <a href="#" class="underline decoration-white/40 underline-offset-2 transition-colors duration-150 hover:decoration-[#50e3c2] hover:text-[#50e3c2] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#50e3c2]">upgrade plan</a>
      </p>
    </li>

    <li class="py-4" data-meter data-name="Function invocations" data-used="8214930" data-limit="10000000" data-unit="invocations">
      <div class="flex items-baseline justify-between gap-4">
        <p class="text-sm font-medium text-white">Function invocations</p>
        <p class="text-sm tabular-nums text-white/50">
          <span data-meter-value class="text-white">8,214,930</span>&nbsp;of&nbsp;<span data-meter-limit>10,000,000</span>&nbsp;invocations
          <span data-meter-percent class="ml-2 text-xs text-white/50">82%</span>
        </p>
      </div>
      <div
        class="mt-2.5 h-1 w-full overflow-hidden rounded-full bg-white/10"
        role="progressbar"
        aria-label="Function invocations"
        aria-valuemin="0"
        aria-valuemax="10000000"
        aria-valuenow="8214930"
        aria-valuetext="8,214,930 of 10,000,000 invocations used, 82 percent"
      >
        <div data-meter-fill class="h-full w-[82%] rounded-full bg-[#50e3c2] transition-[width] duration-700 ease-out"></div>
      </div>
      <p data-meter-note class="mt-2 hidden items-center gap-1.5 text-xs font-medium text-white" aria-live="polite">
        <span class="h-1.5 w-1.5 rounded-full bg-[#50e3c2]" aria-hidden="true"></span>
        Approaching limit — <a href="#" class="underline decoration-white/40 underline-offset-2 transition-colors duration-150 hover:decoration-[#50e3c2] hover:text-[#50e3c2] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#50e3c2]">upgrade plan</a>
      </p>
    </li>

    <li class="py-4" data-meter data-name="Image optimization" data-used="9412" data-limit="10000" data-unit="transformations">
      <div class="flex items-baseline justify-between gap-4">
        <p class="text-sm font-medium text-white">Image optimization</p>
        <p class="text-sm tabular-nums text-white/50">
          <span data-meter-value class="text-white">9,412</span>&nbsp;of&nbsp;<span data-meter-limit>10,000</span>&nbsp;transformations
          <span data-meter-percent class="ml-2 text-xs text-white/50">94%</span>
        </p>
      </div>
      <div
        class="mt-2.5 h-1 w-full overflow-hidden rounded-full bg-white/10"
        role="progressbar"
        aria-label="Image optimization"
        aria-valuemin="0"
        aria-valuemax="10000"
        aria-valuenow="9412"
        aria-valuetext="9,412 of 10,000 transformations used, 94 percent"
      >
        <div data-meter-fill class="h-full w-[94%] rounded-full bg-[#50e3c2] transition-[width] duration-700 ease-out"></div>
      </div>
      <p data-meter-note class="mt-2 flex items-center gap-1.5 text-xs font-medium text-white" aria-live="polite">
        <span class="h-1.5 w-1.5 rounded-full bg-[#50e3c2]" aria-hidden="true"></span>
        Approaching limit — <a href="#" class="underline decoration-white/40 underline-offset-2 transition-colors duration-150 hover:decoration-[#50e3c2] hover:text-[#50e3c2] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#50e3c2]">upgrade plan</a>
      </p>
    </li>

    <li class="py-4" data-meter data-name="Build execution" data-used="187" data-limit="6000" data-unit="minutes">
      <div class="flex items-baseline justify-between gap-4">
        <p class="text-sm font-medium text-white">Build execution</p>
        <p class="text-sm tabular-nums text-white/50">
          <span data-meter-value class="text-white">187</span>&nbsp;of&nbsp;<span data-meter-limit>6,000</span>&nbsp;minutes
          <span data-meter-percent class="ml-2 text-xs text-white/50">3%</span>
        </p>
      </div>
      <div
        class="mt-2.5 h-1 w-full overflow-hidden rounded-full bg-white/10"
        role="progressbar"
        aria-label="Build execution"
        aria-valuemin="0"
        aria-valuemax="6000"
        aria-valuenow="187"
        aria-valuetext="187 of 6,000 minutes used, 3 percent"
      >
        <div data-meter-fill class="h-full w-[3%] rounded-full bg-[#50e3c2] transition-[width] duration-700 ease-out"></div>
      </div>
      <p data-meter-note class="mt-2 hidden items-center gap-1.5 text-xs font-medium text-white" aria-live="polite">
        <span class="h-1.5 w-1.5 rounded-full bg-[#50e3c2]" aria-hidden="true"></span>
        Approaching limit — <a href="#" class="underline decoration-white/40 underline-offset-2 transition-colors duration-150 hover:decoration-[#50e3c2] hover:text-[#50e3c2] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#50e3c2]">upgrade plan</a>
      </p>
    </li>
  </ul>

  <footer class="mt-6 flex flex-wrap items-center justify-between gap-3">
    <p class="text-xs text-white/50">Cycle resets in 8 days. Usage is measured across all projects in this team.</p>
    <a
      href="#"
      class="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-[#50e3c2] transition-colors duration-150 hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#50e3c2]"
    >
      View detailed usage
      <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
        <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
      </svg>
    </a>
  </footer>
</section>

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

    const NEAR_LIMIT = 90;
    const numberFormat = new Intl.NumberFormat('en-US');
    const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');

    function hydrateMeter(meter, animate) {
      const used = Number(meter.dataset.used) || 0;
      const limit = Number(meter.dataset.limit) || 100;
      const unit = meter.dataset.unit || '';
      const name = meter.dataset.name || 'Usage';
      const percent = Math.max(0, Math.min(100, Math.round((used / limit) * 100)));

      const fill = meter.querySelector('[data-meter-fill]');
      const valueEl = meter.querySelector('[data-meter-value]');
      const limitEl = meter.querySelector('[data-meter-limit]');
      const percentEl = meter.querySelector('[data-meter-percent]');
      const bar = meter.querySelector('[role="progressbar"]');
      const note = meter.querySelector('[data-meter-note]');

      valueEl.textContent = numberFormat.format(used);
      limitEl.textContent = numberFormat.format(limit);
      percentEl.textContent = percent + '%';

      bar.setAttribute('aria-valuemax', String(limit));
      bar.setAttribute('aria-valuenow', String(used));
      bar.setAttribute(
        'aria-valuetext',
        numberFormat.format(used) + ' of ' + numberFormat.format(limit) + ' ' + unit + ' used, ' + percent + ' percent'
      );

      if (percent >= NEAR_LIMIT) {
        note.classList.remove('hidden');
        note.classList.add('flex');
      } else {
        note.classList.add('hidden');
        note.classList.remove('flex');
      }

      const target = percent + '%';
      if (animate && !reduceMotion.matches) {
        fill.style.width = '0%';
        requestAnimationFrame(function () {
          requestAnimationFrame(function () {
            fill.style.width = target;
          });
        });
      } else {
        fill.style.width = target;
      }
    }

    const meters = Array.prototype.slice.call(root.querySelectorAll('[data-meter]'));
    const refreshButton = root.querySelector('[data-usage-refresh]');
    const refreshIcon = root.querySelector('[data-usage-refresh-icon]');
    const updatedLabel = root.querySelector('[data-usage-updated]');

    // Hydrate on load with the entrance animation
    meters.forEach(function (meter) { hydrateMeter(meter, true); });

    // Refresh button re-runs hydration (swap in a real fetch for live data)
    let refreshing = false;
    refreshButton.addEventListener('click', function () {
      if (refreshing) return;
      refreshing = true;
      refreshButton.setAttribute('aria-busy', 'true');
      refreshButton.disabled = true;
      if (!reduceMotion.matches) refreshIcon.classList.add('animate-spin');

      window.setTimeout(function () {
        meters.forEach(function (meter) { hydrateMeter(meter, true); });
        updatedLabel.textContent = 'Updated just now';
        refreshIcon.classList.remove('animate-spin');
        refreshButton.removeAttribute('aria-busy');
        refreshButton.disabled = false;
        refreshing = false;
      }, reduceMotion.matches ? 150 : 900);
    });
  })();
</script>
238 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/usage-meter

Continue browsing

View all