A pricing section where the plan picker is one giant word. Click the plan printed above or below, or press ↑ ↓, and the reel turns it into the middle and inks it in, while the card beside it rolls the price and the monthly photo count with an Odometer. A monthly or yearly switch rolls the price too.
registry/blocks/echo-reel-pricing.tsx
"use client";
import * as React from "react";
import { EchoReel } from "@/components/ui/echo-reel";
import { Odometer } from "@/components/ui/odometer";
/* ── Content ─────────────────────────────────────────────────
* Everything you'd change to make the section yours: the
* palette, the plans and the copy. The giant word is the plan
* picker: click the word above or below, or focus it and use
* the arrow keys, and the card beside it rolls to that plan.
* ─────────────────────────────────────────────────────────── */
const PALETTE = {
light: { paper: "#ffffff", ink: "#0b0b10", accent: "#5b2bff", card: "#f6f5fb" },
dark: { paper: "#07070c", ink: "#f5f5f7", accent: "#9b83ff", card: "#111018" },
};
const EYEBROW = "Pricing";
const HEADLINE = "Pay for photos, not seats.";
const HINT = "Pick a plan on the reel, or focus it and press ↑ ↓";
const YEARLY_SAVING = 0.2;
const PLANS = [
{
name: "Starter",
monthly: 19,
photos: 60,
blurb: "For a shop with a few new products a month.",
features: ["Cutouts and soft shadows", "Every marketplace crop", "Alt text in English", "Exports up to 2K", "Email support"],
cta: { label: "Start with Starter", href: "#starter" },
},
{
name: "Studio",
monthly: 79,
photos: 400,
blurb: "For brands shooting a full catalog every season.",
features: ["Everything in Starter", "Lifestyle scenes, lit to match", "12 colorways from one shot", "Alt text in 9 languages", "4K exports for print"],
cta: { label: "Try Studio free for 14 days", href: "#studio" },
},
{
name: "Brand",
monthly: 249,
photos: 2000,
blurb: "For teams feeding a store, ads and wholesale at once.",
features: ["Everything in Studio", "Your own trained look", "Bulk uploads from a folder", "Five seats, review links", "A shoot lead on Slack"],
cta: { label: "Talk to us about Brand", href: "#brand" },
},
];
/* ── Section ─────────────────────────────────────────────── */
const usd: Intl.NumberFormatOptions = { style: "currency", currency: "USD", maximumFractionDigits: 0 };
export function EchoReelPricing() {
const [plan, setPlan] = React.useState(1);
const [yearly, setYearly] = React.useState(true);
const current = PLANS[plan];
const price = Math.round(current.monthly * (yearly ? 1 - YEARLY_SAVING : 1));
return (
<section
style={
{
"--paper-light": PALETTE.light.paper,
"--ink-light": PALETTE.light.ink,
"--accent-light": PALETTE.light.accent,
"--card-light": PALETTE.light.card,
"--paper-dark": PALETTE.dark.paper,
"--ink-dark": PALETTE.dark.ink,
"--accent-dark": PALETTE.dark.accent,
"--card-dark": PALETTE.dark.card,
} as React.CSSProperties
}
className="relative overflow-hidden bg-(--paper) text-(--ink) [--accent:var(--accent-light)] [--card:var(--card-light)] [--ink:var(--ink-light)] [--paper:var(--paper-light)] dark:[--accent:var(--accent-dark)] dark:[--card:var(--card-dark)] dark:[--ink:var(--ink-dark)] dark:[--paper:var(--paper-dark)]"
>
<div className="mx-auto max-w-6xl px-6 py-24 sm:py-28">
<div className="flex flex-col items-start justify-between gap-6 sm:flex-row sm:items-end">
<div>
<p className="text-sm font-semibold text-(--accent)">{EYEBROW}</p>
<h2 className="mt-3 text-4xl font-semibold tracking-[-0.04em] text-balance sm:text-5xl">{HEADLINE}</h2>
</div>
<div role="group" aria-label="Billing" className="flex rounded-full bg-(--card) p-1 text-sm font-medium">
{[false, true].map((y) => (
<button
key={String(y)}
type="button"
aria-pressed={yearly === y}
onClick={() => setYearly(y)}
className="flex h-9 items-center gap-1.5 rounded-full px-4 transition-[background-color,color,box-shadow] duration-200 ease-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) aria-pressed:bg-(--paper) aria-pressed:shadow-[0_1px_2px_rgb(0_0_0/0.08),0_0_0_1px_rgb(0_0_0/0.04)] [&:not([aria-pressed=true])]:opacity-60"
>
{y ? "Yearly" : "Monthly"}
{y && <span className="rounded-full bg-(--accent) px-1.5 py-0.5 text-[10px] leading-none font-semibold text-white dark:text-[#07070c]">−20%</span>}
</button>
))}
</div>
</div>
<div className="mt-12 grid items-center gap-10 lg:mt-16 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)] lg:gap-14">
<div>
<EchoReel
words={PLANS.map((p) => p.name)}
value={plan}
onValueChange={(index) => setPlan(index)}
interval={false}
className="text-(--accent)"
/>
<p className="mt-4 text-center text-[13px] opacity-55">{HINT}</p>
</div>
<div className="rounded-[28px] bg-(--card) p-7 sm:p-8">
<div className="flex items-baseline justify-between gap-4">
<p className="text-sm font-semibold">{current.name}</p>
<p className="text-sm opacity-60">
<Odometer value={current.photos} className="font-semibold tabular-nums opacity-100" /> photos a month
</p>
</div>
<p className="mt-5 flex items-baseline gap-1.5">
<Odometer value={price} format={usd} className="text-5xl font-semibold tracking-[-0.04em]" />
<span className="text-sm opacity-60">/month</span>
</p>
<p className="mt-1.5 text-[13px] opacity-60">
{yearly ? `Billed $${(price * 12).toLocaleString("en-US")} a year` : "Billed monthly, cancel anytime"}
</p>
<p className="mt-5 text-[15px] leading-relaxed opacity-80">{current.blurb}</p>
<ul className="mt-5 space-y-2.5 text-[14px]">
{current.features.map((feature) => (
<li key={feature} className="flex gap-2.5">
<svg viewBox="0 0 16 16" aria-hidden className="mt-0.5 size-4 shrink-0 text-(--accent)">
<path d="M3.5 8.5l3 3 6-7" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
{feature}
</li>
))}
</ul>
<a
href={current.cta.href}
className="mt-7 flex h-12 items-center justify-center rounded-full bg-(--accent) px-6 text-[15px] font-semibold text-white transition-[opacity,scale] duration-150 hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) active:scale-[0.98] dark:text-[#07070c]"
>
{current.cta.label}
</a>
</div>
</div>
</div>
</section>
);
}
Live preview and the components it's built with: https://keyframeui.com/blocks/pricing-sections/echo-reel-pricing