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

Free tier Pro lifts the ceiling
6 of 25 expenses Cap: 25/month
Free tier Pro lifts the ceiling
15 of 25 expenses Cap: 25/month
Free tier Pro lifts the ceiling
23 of 25 expenses Cap: 25/month

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

Before scroll (hidden fill)
Free tier Pro lifts the ceiling
15 of 25 expenses Cap: 25/month
After scroll (fill animated in)
Free tier Pro lifts the ceiling
15 of 25 expenses Cap: 25/month
StateFill widthTransition
Before viewport0% (CSS sets fill to 0 initially)
Entered viewport--meter-fill CSS variable (e.g. 60%)420ms --ease-out

On dark surfaces

Free tier Pro lifts the ceiling
15 of 25 expenses Cap: 25/month

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

Free tier Pro lifts the ceiling
15 of 25 expenses Cap: 25/month
Eyebrow labels (.cap-meter-labels)
Fill (.cap-meter-fill)
Meta row (.cap-meter-meta)
PartElementNotes
Container.cap-meterFull width. Holds all sub-parts. CSS custom property --meter-fill controls fill percentage.
Labels row.cap-meter-labelsFlex row, space-between. Mono eyebrow text. "FREE TIER" left, "PRO LIFTS THE CEILING" right — uppercase only for this eyebrow style.
Track.cap-meter-track8px tall, pill-shaped, warm background. Contains the fill.
Fill.cap-meter-fillTeal-to-coral gradient. Starts at 0%. JS adds .animate to parent on scroll to trigger the width transition.
Meta row.cap-meter-metaUsage count left, cap label right. Used count in bold ink.

Usage

Do Use this component exclusively for the free-tier cap metric. Set --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.
Don't Don't use this for generic progress (use the Progress component). Don't skip the animation — the fill animating from 0 is part of the delight. Don't hardcode the fill width; always use --meter-fill so the value is data-driven.
Do Keep label copy consistent with the landing: "FREE TIER" left, "PRO LIFTS THE CEILING" right. These exact strings are part of the brand voice ("caps, not locks") — don't substitute synonyms.
Don't Don't add urgency language ("almost full!", "upgrade now!") to the meter — the gradient is the tone signal. The meter should feel informative, not pushy. The product nudges, never nags.

Accessibility

ARIA Add 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">.
Motion sensitivity The animation is triggered on scroll. Respect prefers-reduced-motion — when set, apply the final fill width immediately without transition: @media (prefers-reduced-motion: reduce) { .cap-meter-fill { transition: none; } }.
Color independence The gradient color shift (teal to coral) is a redundant signal. The meta row text ("15 of 25 expenses") is the primary numerical indicator and must always be present.

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

ClassPurpose
.cap-meterRoot container. Set --meter-fill here as a percentage string.
.cap-meter.animateTriggers the fill transition (added by JS on scroll-in)
.cap-meter-labelsRow 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-track8px pill track — also holds ARIA progressbar role
.cap-meter-fillGradient fill — width driven by --meter-fill
.cap-meter-metaBottom row with usage count and cap text

Design tokens used

TokenValueRole
--teal#0E7C66Gradient start (left, low usage)
--coral#E76F51Gradient end (right, near cap)
--s-warm#F1E9D5Track background
--dark-elevated#2A2521Track background on dark
--font-monoJetBrains MonoEyebrow labels and cap text
--d-slow420msFill animation duration
--ease-outcubic-bezier(0.23,1,0.32,1)Fill animation easing