---
title: Usage Meter
description: Usage-vs-limit meter with threshold intent, panel grid, and plan-paywall gate
visibility: guest
draft: false
---

# Usage Meter

A meter for showing how much of a plan limit has been consumed — seats, API
calls, storage, projects. It pairs a labeled bar with a `used / limit (%)`
readout, switches color as usage approaches the limit, and conveys the
over-limit state by **text** (not color alone). Two sub-components round out
the SaaS billing surface: `usage-meter.panel` (a responsive grid of meters)
and `usage-meter.gate` (a plan-paywall wrapper that disables an action once
the limit is reached).

The bar itself is a composed [`<x-wirekit::progress>`](/components/progress),
so it inherits the `role="progressbar"` WCAG contract for free.

## Basic Usage

:::preview{title="A single usage meter at 60%"}
<x-wirekit::usage-meter label="Projects" :used="6" :limit="10" unit="projects" />
:::

The readout shows `6 / 10 projects (60%)`. Pass `used` and `limit` from your
Livewire component — the meter never computes usage itself.

## Threshold States

As usage climbs, the intent shifts `primary → warning → danger`. Crossing the
warn threshold (default `0.8`) adds an **"Approaching limit"** line; reaching
or exceeding the limit replaces it with **"Over limit"**. Both states are
spelled out in words so the signal never depends on color.

:::preview{title="Ok, approaching, and over-limit meters"}
<x-wirekit::stack gap="lg">
    <x-wirekit::usage-meter label="Storage — comfortable" :used="40" :limit="100" unit="GB" />
    <x-wirekit::usage-meter label="Storage — approaching" :used="90" :limit="100" unit="GB" />
    <x-wirekit::usage-meter label="Storage — over limit" :used="112" :limit="100" unit="GB" />
</x-wirekit::stack>
:::

The over-limit meter shows the true overage (`112%`) in the readout while the
bar clamps to 100% — the number tells the real story, the bar stays in bounds.

## Unlimited Tier

Pass `:limit="null"` for an unlimited plan. The bar is omitted (there is no
meaningful fill) and the readout reads `Unlimited`.

:::preview{title="An unlimited plan tier"}
<x-wirekit::usage-meter label="Team members" :used="42" :limit="null" unit="members" />
:::

## Reset Cadence

A `period` of `day` / `week` / `month` / `year` renders a subtle reset note.
An explicit `resetAt` string overrides the derived note for an exact date.

:::preview{title="Reset cadence hints"}
<x-wirekit::stack gap="md">
    <x-wirekit::usage-meter label="API calls" :used="7200" :limit="10000" period="month" />
    <x-wirekit::usage-meter label="Exports" :used="3" :limit="5" resetAt="Resets Jan 1, 2027" />
</x-wirekit::stack>
:::

## Upgrade Call To Action

The `action` slot hangs an upgrade prompt off a meter — handy the moment a
plan is nearing its ceiling.

:::preview{title="Meter with an upgrade prompt"}
<x-wirekit::usage-meter label="Seats" :used="5" :limit="5" unit="seats">
    <x-slot:action>
        <x-wirekit::button intent="primary" size="sm" href="#upgrade">Upgrade plan</x-wirekit::button>
    </x-slot:action>
</x-wirekit::usage-meter>
:::

## Panel — A Grid Of Meters

`usage-meter.panel` lays out several meters in a responsive grid (1, 2, or 3
columns; collapses to a single column on small screens). Give it a `title` and
it becomes a labeled group.

:::preview{title="A plan-usage dashboard panel"}
<x-wirekit::usage-meter.panel title="Plan usage this month" :columns="2">
    <x-wirekit::usage-meter label="Projects" :used="8" :limit="10" unit="projects" period="month" />
    <x-wirekit::usage-meter label="Seats" :used="3" :limit="5" unit="seats" />
    <x-wirekit::usage-meter label="API calls" :used="9400" :limit="10000" period="month" />
    <x-wirekit::usage-meter label="Storage" :used="112" :limit="100" unit="GB" />
</x-wirekit::usage-meter.panel>
:::

## Gate — Plan Paywall

`usage-meter.gate` wraps an action that should be blocked once a limit is
reached. While under limit, it renders its content untouched. When `over` is
true, the content is dimmed and made `inert` (skipped by assistive tech and
unclickable), and the `reason` plus an optional upgrade `action` slot explain
why — and how to lift the gate.

:::preview{title="A gated action at the plan ceiling"}
<x-wirekit::usage-meter.gate :over="true" reason="You've reached your 10-project limit.">
    <x-wirekit::button intent="primary">Create project</x-wirekit::button>
    <x-slot:action>
        <x-wirekit::button intent="primary" surface="outline" size="sm" href="#upgrade">Upgrade plan</x-wirekit::button>
    </x-slot:action>
</x-wirekit::usage-meter.gate>
:::

## Livewire Integration

Bind `used` and `limit` to your component's state. As usage updates, the meter
re-renders with the right threshold intent automatically:

```blade
{{-- 1. The meter reads usage straight off the component --}}
<x-wirekit::usage-meter
    label="API calls"
    :used="$usage['calls']"
    :limit="$plan->callLimit"
    unit="calls"
    period="month"
/>

{{-- 2. Gate the action behind the limit --}}
<x-wirekit::usage-meter.gate
    :over="$usage['projects'] >= $plan->projectLimit"
    reason="You've reached your project limit."
>
    <x-wirekit::button wire:click="createProject">New project</x-wirekit::button>
    <x-slot:action>
        <x-wirekit::button surface="outline" size="sm" href="{{ route('billing') }}">Upgrade</x-wirekit::button>
    </x-slot:action>
</x-wirekit::usage-meter.gate>
```

```php
// 3. Supply usage + limits from the server — never recompute in the view
public array $usage = ['calls' => 7200, 'projects' => 10];

public function createProject(): void
{
    // The gate already blocks the click in the UI; re-check on the server too.
    abort_if($this->usage['projects'] >= $this->plan->projectLimit, 403);
    // …create…
}
```

## Props

### `usage-meter`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | string\|null | `null` | Visible metric name above the bar |
| `used` | numeric | `0` | Current usage (supplied by the app) |
| `limit` | numeric\|null | `null` | Plan limit; `null` = unlimited tier |
| `unit` | string\|null | `null` | Unit suffix in the readout (`seats`, `GB`) |
| `period` | string\|null | `null` | Reset cadence: `day` / `week` / `month` / `year` |
| `resetAt` | string\|null | `null` | Explicit reset note (overrides `period`) |
| `warn` | float | `0.8` | Ratio at which the warning band starts |
| `danger` | float | `1.0` | Ratio at which the danger/over band starts |
| `showValue` | bool | `true` | Show the `used / limit (%)` readout |
| `scope` | string\|null | `null` | Scoped personalization name |

### `usage-meter.panel`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | string\|null | `null` | Optional group heading above the grid |
| `columns` | int | `1` | Column count on wider viewports: `1`, `2`, `3` |
| `scope` | string\|null | `null` | Scoped personalization name |

### `usage-meter.gate`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `over` | bool | `false` | `true` blocks the action and shows the reason |
| `reason` | string\|null | `null` | Why the action is blocked (paired with `aria-disabled`) |
| `scope` | string\|null | `null` | Scoped personalization name |

## Accessibility

- The bar is a composed [`<x-wirekit::progress>`](/components/progress), so it
  carries `role="progressbar"` with `aria-valuenow` / `aria-valuemin` /
  `aria-valuemax`. Screen readers announce the current usage.
- A visible `label` is linked to the bar via `aria-labelledby`; without one,
  the bar falls back to `aria-label="Usage"`.
- The over-limit and approaching-limit states are conveyed in **words**, never
  by color alone — the colored token is redundant reinforcement on top of the
  text. This satisfies WCAG 1.4.1 (Use of Color).
- `usage-meter.gate` marks the gated action `aria-disabled="true"` **and**
  `inert`, so it is skipped by assistive tech and cannot receive focus or
  clicks. The `reason` text is the accessible explanation that pairs with the
  disabled state.

## Keyboard Interaction

The meter and panel are presentational and do not respond to keyboard input.
The gate's wrapped action is `inert` while over limit, so keyboard users tab
straight past it to the upgrade CTA.

## Pitfalls

- **Supply `used` and `limit` from the server.** The meter never recomputes
  usage — it renders exactly what you pass. Compute the usage in your Livewire
  component (or a cached aggregate) and bind it.
- **The gate is a UI affordance, not a security boundary.** Always re-check the
  limit on the server in the action handler — a determined user can bypass any
  client-side disable.
- **Use `:limit="null"` for unlimited, not a huge number.** A sentinel like
  `999999` renders a misleading near-empty bar; `null` renders the proper
  `Unlimited` tier with no bar.

## Design Tokens

| Element | Token |
| --- | --- |
| Label color | `--color-wk-text` |
| Readout color | `--color-wk-text-muted` |
| Percentage / reset note | `--color-wk-text-subtle` |
| "Over limit" text | `--color-wk-danger-text` |
| "Approaching limit" text | `--color-wk-warning-text` |
| Bar fill (under warn) | `--color-wk-accent` |
| Bar fill (warning band) | `--color-wk-warning` |
| Bar fill (danger / over) | `--color-wk-danger` |
| Panel grid gap | `--space-wk-md` |
| Gate disabled opacity | `--opacity-wk-disabled` |

## Customization

Override the threshold defaults without publishing views via
`config/wirekit.php`:

```php
'components' => [
    'usage-meter' => [
        'warn' => 0.75,   // start the warning band at 75%
        'danger' => 0.95, // start the danger band at 95%
    ],
],
```

## Further Reading

- [WAI-ARIA Authoring Practices — Meter / Progress](https://www.w3.org/WAI/ARIA/apg/patterns/meter/)
- [WCAG 1.4.1 — Use of Color](https://www.w3.org/WAI/WCAG21/Understanding/use-of-color.html)
- [MDN: the `inert` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inert)
