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

What it does

Capabilities

  • Minimal Vercel design language
  • Refresh action with aria-busy and spin feedback

Preview

Live component

Interactive preview
Open preview
9:41 100%
devsnips.site/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
DevSnips AI ready Claude · Cursor · Windsurf

Agent skill & Model Context Protocol

Pass this component straight into your AI workflow. DevSnips exposes each resource as a structured, addressable registry entry, so your agent installs the real files instead of approximating them.

devsnips://components/progress/usage-meter
Prompt instruction
I want to use this DevSnips resource in production:

Page:
https://devsnips.site/library/tailwind/components/progress/usage-meter/

Install: npx devsnips add
Tailwind/Components/Progress/usage-meter

This is the 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.

Use this DevSnips resource as the starting point for my project.
Adapt it to my project's stack and conventions, fix any issues,
and make it production-ready while preserving its intended design
and behavior.

Paste into ChatGPT, Claude or your agent — it already carries the page URL and the exact install path.

Resource URI
devsnips://components/progress/usage-meter