Cap meter
A horizontal usage bar that communicates how much of the free tier a group has consumed. Animates from 0% on scroll-in so it feels earned. The teal-to-coral gradient conveys progression from comfortable to approaching the cap — without guilt. Used in the pricing section of the landing and in the in-app settings screen.
Variants
The gradient runs teal (left, comfortable) to coral (right, approaching cap). The fill reflects actual usage as a percentage. As usage climbs, the color shifts — the visual message shifts from calm to attentive without an alert or warning tone.
The label pair "FREE TIER" / "PRO LIFTS THE CEILING" uses the JetBrains Mono eyebrow style — uppercase mono, 10px, 0.1em tracking. This is consistent with the landing's pricing section header treatment and is one of the permitted uppercase patterns in the design language.
States
| State | Fill width | Transition |
|---|---|---|
| Before viewport | 0% (CSS sets fill to 0 initially) | — |
| Entered viewport | --meter-fill CSS variable (e.g. 60%) | 420ms --ease-out |
On dark surfaces
On the landing's dark "moments" section, the track background changes to --dark-elevated. The gradient fill is unchanged — teal and coral both have strong contrast on dark. Label text uses dark-text tokens. Add .on-dark to the ancestor element.
Anatomy
| Part | Element | Notes |
|---|---|---|
| Container | .cap-meter | Full width. Holds all sub-parts. CSS custom property --meter-fill controls fill percentage. |
| Labels row | .cap-meter-labels | Flex row, space-between. Mono eyebrow text. "FREE TIER" left, "PRO LIFTS THE CEILING" right — uppercase only for this eyebrow style. |
| Track | .cap-meter-track | 8px tall, pill-shaped, warm background. Contains the fill. |
| Fill | .cap-meter-fill | Teal-to-coral gradient. Starts at 0%. JS adds .animate to parent on scroll to trigger the width transition. |
| Meta row | .cap-meter-meta | Usage count left, cap label right. Used count in bold ink. |
Usage
--meter-fill on the element as a percentage (e.g. style="--meter-fill: 60%"). Trigger the animation via IntersectionObserver to add .animate only when visible.
--meter-fill so the value is data-driven.
Accessibility
role="progressbar", aria-valuenow, aria-valuemin="0", and aria-valuemax="100" to .cap-meter-track. Also add aria-label="Free tier usage: 60%" so the reading is unambiguous. Example: <div class="cap-meter-track" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" aria-label="Free tier usage: 60 percent">.
prefers-reduced-motion — when set, apply the final fill width immediately without transition: @media (prefers-reduced-motion: reduce) { .cap-meter-fill { transition: none; } }.
Code
HTML
<div class="cap-meter" style="--meter-fill: 60%"> <div class="cap-meter-labels"> <span class="cap-meter-label-left">Free tier</span> <span class="cap-meter-label-right">Pro lifts the ceiling</span> </div> <div class="cap-meter-track" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" aria-label="Free tier usage: 60 percent"> <div class="cap-meter-fill"></div> </div> <div class="cap-meter-meta"> <span class="cap-meter-used"><strong>15</strong> of 25 expenses</span> <span class="cap-meter-limit">Cap: 25/month</span> </div> </div> // Scroll-triggered animation const observer = new IntersectionObserver( (entries) => { entries.forEach((e) => { if (e.isIntersecting) e.target.classList.add("animate"); }); }, { threshold: 0.3 } ); document.querySelectorAll(".cap-meter").forEach((el) => observer.observe(el));
CSS classes
| Class | Purpose |
|---|---|
.cap-meter | Root container. Set --meter-fill here as a percentage string. |
.cap-meter.animate | Triggers the fill transition (added by JS on scroll-in) |
.cap-meter-labels | Row holding the two eyebrow labels |
.cap-meter-label-left | "Free tier" label — mono uppercase |
.cap-meter-label-right | "Pro lifts the ceiling" label — mono uppercase |
.cap-meter-track | 8px pill track — also holds ARIA progressbar role |
.cap-meter-fill | Gradient fill — width driven by --meter-fill |
.cap-meter-meta | Bottom row with usage count and cap text |
Design tokens used
| Token | Value | Role |
|---|---|---|
--teal | #0E7C66 | Gradient start (left, low usage) |
--coral | #E76F51 | Gradient end (right, near cap) |
--s-warm | #F1E9D5 | Track background |
--dark-elevated | #2A2521 | Track background on dark |
--font-mono | JetBrains Mono | Eyebrow labels and cap text |
--d-slow | 420ms | Fill animation duration |
--ease-out | cubic-bezier(0.23,1,0.32,1) | Fill animation easing |