9.3 KiB
Pricing
Function
The plan-comparison section: a billing-period toggle over a row of plans, one usually featured. It answers "what does it cost" and lets the visitor flip monthly ↔ annual with the prices updating in step.
Composition map
| Part | Composes | Notes |
|---|---|---|
| root | bare <section> + Section + Container + Stack |
bind:period; provides the period context |
.Header |
Box + Stack |
the app's section Heading + Text |
.Switch |
ToggleGroup |
writes the period; reads it back to stay in sync |
.Plans |
AutoGrid + data-stagger |
fluid row of equal-height plan cards; the stagger rhythm is structural. maxWidth caps and centres the row for short plan counts (V8) |
.Plan |
Motion (trigger="viewport") wrapping a Card (outline) |
IS the reveal (direct child of the staggered row); featured → accent colour; badge chip |
.PlanName |
Heading (level 3) |
|
.PlanDescription |
Text (muted, sm) |
one-line tagline |
.PlanPrice |
Group + Text |
shows the monthly / annual snippet per the period; suffix beside it |
.PlanFeatures / .PlanFeature |
Stack / Group + Icon.Check + Text |
the checklist; the check is decorative (aria-hidden) |
.PlanAction |
Box (margin-block-start: auto) |
the CTA, pinned to the card's bottom so a row lines up |
Landmark + headings: a bare <section> with NO accessible name of its own,
so it is not an exposed landmark — the app names it by passing aria-label /
aria-labelledby through ...rest. The section heading is the app's (h2, in
.Header); .PlanName emits h3 and takes a level prop to move under a
different parent.
Why compound WITH a context
feature-grid and feature-split are compound because their parts repeat
but do not coordinate. Pricing is the tier's first compound whose parts
talk to each other: the Switch writes the billing period and every
PlanPrice reads it. That coordination is exactly what earns a shared context
(context.ts) — the admission rule for the strongest form of compound.
The block never formats currency. PlanPrice takes monthly / annual
snippets; the app composes FormatNumber (locale, currency, rounding) inside
them. The block owns the switching, not the money.
Coordination
Position under the 2026-07-31 doctrine (architecture/blocks.md §«Coordination»).
It coordinates, and it already did before the doctrine had a name. The
billing period travels by context from .Switch to every PlanPrice, so the
app never re-derives which price to show — the case §«Coordination» describes.
What it does NOT need is a state machine or words of its own: nothing in a pricing table can be blocked, so there is no reason to explain and no mute state to make impossible. Coordination is not always a machine.
Decisions
2026-07-24 — reference floor (dossier §P1: Tailwind Plus «Pricing 12» · Untitled UI «Pricing 22» · Flowbite):
- Adopted: the monthly/annual toggle, the featured plan, and the per-plan feature checklist + CTA — the dossier's "toggle + featured = paridad OK". The prices switch through a shared context so the toggle is honest.
- The app owns the money:
FormatNumberin the price snippets, per B-7.periodis bindable so the app can observe (analytics, a persisted choice). - Discarded: static dumps; the plans are the app's markup, not a variant matrix.
Demo
web/routes/blocks/pricing/ — the block full-bleed, three plans (featured in
the middle), the live monthly/annual toggle switching every price. Mini-page in
PricingSite.svelte, shared by both surfaces.
Gaps
| Gap | Disposition |
|---|---|
| Feature-comparison table (features × plans matrix) | scope-approval pending — the dossier's recurring pricing gap. It is a table, not a row of cards, so likely a sibling (pricing-table) or a .Compare sub-block — a user decision, not a silent add |
| Single-price variant (one plan, centered) | SHIPPED 2026-08-17 (F2b · V8) — as .Plans maxWidth. The old disposition here («the layout already handles it») was WRONG and is kept visible on purpose; see «The row cap» below |
| Per-period savings note ("2 meses gratis") | app-land — the app puts it in the annualLabel or a .PlanDescription |
| Elevation on the featured plan | canon candidate — Card exposes no elevation prop, and a block paints nothing of its own. Marking the recommended tier by depth (not just colour) needs the prop in Card; until then featured is colour only |
The row cap (.Plans maxWidth) — 2026-08-17, plan F2b · V8
This row existed in the Gaps table as «deferred — one .Plan in .Plans; the
layout already handles it». It did not. Measured before touching anything,
at 1280 with the lg measure (992px of row):
| Arrangement | What it measured |
|---|---|
| one plan, as it was | card of 315px at the start edge, 677px of emptiness beside it |
one plan, columns={1} |
one card 992px wide — a poster, not a price card |
one plan, row capped at 28rem |
card of 448px, centred — the arrangement the references ship |
one plan, inside Container sm |
640px still fits two tracks → card of 308px with 332px of emptiness |
The cause is in the track list, not in the plan count: the fluid row is
repeat(auto-fill, minmax(17rem, 1fr)), and auto-fill RESERVES every track
that fits whether or not a child occupies it. So the fix is a cap on the ROW,
which the part now takes as maxWidth and forwards to the AutoGrid
explicitly — never through {...rest}, because these props are closed on
purpose (A-94) and a promised surface has to be honoured. marginX="auto" goes
with it, unconditionally: with no cap the row already fills the measure and
auto resolves to nothing.
The number is the app's. A block that baked one would be deciding how wide a
plan looks; the demo passes 28rem for one plan and 44rem for two, which is
what the references give each.
Measured after: one plan 448px centred · two plans 704px (two 340px tracks), centred · three plans 992px, byte-identical to before — the cap is opt-in and changes nothing when it is absent · at 375 the cap is inert, since a single track already takes the width.
Found while composing
- A «deferred» disposition can be a wrong measurement, not a postponed one (2026-08-17). The single-price row said the layout already covered it, and the layout did the opposite — reserving two empty tracks next to the only card. Nobody had measured it: the block shipped with three plans and was only ever looked at with three. Worth remembering when reading any other row of this table.