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>,
so it inherits the role="progressbar" WCAG contract for free.
Basic Usage
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.
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.
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.
Upgrade Call To Action
The action slot hangs an upgrade prompt off a meter — handy the moment a
plan is nearing its ceiling.
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.
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.
Livewire Integration
Bind used and limit to your component's state. As usage updates, the meter
re-renders with the right threshold intent automatically:
{{-- 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>
// 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>, so it carriesrole="progressbar"witharia-valuenow/aria-valuemin/aria-valuemax. Screen readers announce the current usage. - A visible
labelis linked to the bar viaaria-labelledby; without one, the bar falls back toaria-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.gatemarks the gated actionaria-disabled="true"andinert, so it is skipped by assistive tech and cannot receive focus or clicks. Thereasontext 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
usedandlimitfrom 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 like999999renders a misleading near-empty bar;nullrenders the properUnlimitedtier 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:
'components' => [
'usage-meter' => [
'warn' => 0.75, // start the warning band at 75%
'danger' => 0.95, // start the danger band at 95%
],
],