1,284
Number Flow
DataA number whose digits roll to the new value when it changes.
Usage-based pricing: a slider of monthly usage and every plan priced at the usage picked, each price rolling to its new figure, the cheapest plan for that usage lit and badged. A plan is a base price, what it includes, the rate for each block over it and an optional limit, past which it says so instead of a price. The slider is a group of radios drawn as a track with a thumb, so arrow keys and focus are native; every price at every stop is worked out at build and :has() shows the one picked, and a screen reader hears that price, not the reel. Zero JavaScript.
Pricing
Sample plans and prices, made up for this demo.
Lowest price at this usage
Free
For a side project
$0 / monthPast this plan's limitPast this plan's limitPast this plan's limitPast this plan's limit
$0 includes 3,000 emails
Lowest price at this usage
Pro
For a product
$20 / month$20 / month$65 / month$200 / month$875 / month
$20 includes 50,000 emails, then $0.90 / 1,000 emails
Lowest price at this usage
Scale
For volume
$90 / month$90 / month$90 / month$90 / month$540 / month
$90 includes 250,000 emails, then $0.60 / 1,000 emails
Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.
Variants · each row writes the attribute the prop writes; the section's own CSS answers
JavaScript of its own
0 B
CSS 13.9 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
Chrome 105 · Firefox 121 · Safari 15.4; needs has. Elsewhere: without :has() the first stop's prices stay.
The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:
npx shadcn@latest add @moonarc-pro/pricing-usage---
import PricingUsage from '@/components/moonarc/pro/PricingUsage.astro';
---
<PricingUsage
eyebrow="Pricing"
unit={{ one: 'email', many: 'emails' }}
stops={[3000, 50000, 100000, 500000, 1000000]}
start={1}
plans={[
{ name: 'Free', base: 0, included: 3000, max: 3000, features: ['One domain'], action: { href: '/signup/', label: 'Start free' } },
{ name: 'Pro', base: 20, included: 50000, over: 0.9, per: 1000, features: ['Ten domains', 'Webhooks'], action: { href: '/signup/', label: 'Start with Pro' }, featured: true },
{ name: 'Scale', base: 90, included: 250000, over: 0.6, per: 1000, features: ['Dedicated IP'], action: { href: '/contact/', label: 'Talk to us' } },
]}
>
<span slot="headline">Pay for what you <em>send</em></span>
</PricingUsage>Without a key the registry answers 401 with an upgrade message.
| Prop | Type | Default | Description |
|---|---|---|---|
plans | { name; blurb?; base; included; over?; per?; max?; features?; action; featured? }[] | none | One to four plans. The price at a usage is base plus, for every block of per units (1,000 when unset) over included, over. A plan without over, or past its max, prints the overLimit words instead of a price. |
stops | number[] | none | Two to eight usage values, low to high: the slider's stops. |
start | number | 0 | The stop picked at first, by index. |
unit | { one: string; many: string } | none | The unit, singular and plural. |
currency | string | '$' | The currency symbol, printed before the figure. |
locale | string | 'en-US' | A BCP 47 locale for the numbers (the stops are compact: 50K, 1M). |
words | Partial<{ usage; perPeriod; includes; then; overLimit; best }> | none | Every word the section prints around the figures. |
eyebrow | string | none | A short line above the heading. |
headline | string | none | The heading. Slot "headline" wins and may hold an <em>. |
copy | string | none | A line under the heading. |
level | 2 | 3 | 2 | Heading level of the heading. |
tone | 'surface' | 'muted' | 'ink' | 'brand' | unset (paints nothing) | The section's ground. |
density | 'compact' | 'cozy' | 'roomy' | 'cozy' | Block padding ×0.8 / ×1 / ×1.25 through --ma-density. |
The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.
.ma-pusage.ma-pusage__action.ma-pusage__badge.ma-pusage__blurb.ma-pusage__copy.ma-pusage__eyebrow.ma-pusage__features.ma-pusage__figure.ma-pusage__head.ma-pusage__headline.ma-pusage__inner.ma-pusage__input.ma-pusage__legend.ma-pusage__name.ma-pusage__period.ma-pusage__plan.ma-pusage__plans.ma-pusage__price.ma-pusage__readout.ma-pusage__reel.ma-pusage__slider.ma-pusage__sr.ma-pusage__stop.ma-pusage__terms.ma-pusage__thumb.ma-pusage__track.ma-pusage__window
slot="headline"
The prices change in place without rolling and the thumb moves at once.
ClientRouterCSS only; the stop picked resets on a fresh page.
Replaces: Pricing calculators with a usage slider · Usage sliders on API pricing pages. See the migration table.
1,284
A number whose digits roll to the new value when it changes.
sample prices
Starter
$12/mo$115/yr
one site
Studio
$48/mo$460/yr
up to ten
A monthly / yearly toggle whose prices roll to the other amount instead of swapping.
$0/yr
MIT, forever
$129/yr
One person, billed yearly
$349/yr
Up to 10 people, one invoice
Shows pricing plans side by side, with an annual/monthly toggle that rolls every price digit by digit to the other period with zero JavaScript (radios, :has and NumberFlow), the highlighted plan wearing a travelling border beam, cards revealed with a stagger.
A feature-by-plan comparison: the highlighted plan's column lit, rows revealed with a 40 ms stagger as the table scrolls into view, and a header row that sticks when you cap the table's height.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown