The giant word picks a pricing plan: click the proof above or below, or use the arrow keys, and the price card beside it changes.
registry/demos/echo-reel-plans-demo.tsx
"use client";
import * as React from "react";
import { EchoReel } from "@/components/ui/echo-reel";
const PLANS = [
{
name: "Solo",
price: 12,
blurb: "For one brand you're building yourself.",
lines: [
"One brand kit",
"Logo, palette and type pairings",
"Export to Figma and PDF",
],
cta: "Start with Solo",
},
{
name: "Team",
price: 36,
blurb: "For an in-house team keeping five brands in line.",
lines: [
"Five brand kits, five seats",
"Shared templates and review links",
"Version history on every asset",
],
cta: "Try Team free for 14 days",
},
{
name: "Studio",
price: 96,
blurb: "For agencies handing kits to clients.",
lines: [
"Unlimited client kits, 20 seats",
"White-label handoff pages",
"Priority support and SSO",
],
cta: "Talk to us about Studio",
},
];
function Check() {
return (
<svg
viewBox="0 0 16 16"
aria-hidden
className="mt-0.5 size-4 shrink-0 text-[#ff3d1f] dark:text-[#ff6a4d]"
>
<path
d="M3.5 8.5l3 3 6-7"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export function EchoReelPlansDemo() {
const [plan, setPlan] = React.useState(1);
return (
<section className="@container w-full max-w-4xl overflow-hidden rounded-3xl border border-[#0b0b0d]/10 bg-white px-5 pt-8 pb-8 text-[#0b0b0d] @[44rem]:px-10 @[44rem]:pb-10 dark:border-white/10 dark:bg-[#08080b] dark:text-[#f4f4f6]">
<div className="flex flex-wrap items-baseline justify-between gap-x-6 gap-y-1">
<h2 className="text-lg font-semibold tracking-tight">Tinta pricing</h2>
<p className="text-[13px] text-[#0b0b0d]/55 dark:text-[#f4f4f6]/55">
Click a plan above or below, or use <kbd className="font-sans">↑</kbd>{" "}
<kbd className="font-sans">↓</kbd>
</p>
</div>
<div className="mt-6 grid items-center gap-6 @[44rem]:mt-8 @[44rem]:grid-cols-[minmax(0,1.15fr)_minmax(0,1fr)] @[44rem]:gap-10">
<EchoReel
words={PLANS.map((p) => p.name)}
value={plan}
onValueChange={(index) => setPlan(index)}
interval={false}
aria-label="Plan"
className="mx-auto max-w-md text-[#ff3d1f] dark:text-[#ff5a3a]"
/>
{/* Every plan in one grid cell, so the card keeps its height while they cross. */}
<div className="mx-auto grid w-full max-w-sm">
{PLANS.map((p, i) => {
const on = i === plan;
return (
<div
key={p.name}
aria-hidden={!on}
inert={!on}
className="[grid-area:1/1] rounded-2xl border border-[#0b0b0d]/10 bg-white p-5 shadow-sm transition-[opacity,transform] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-opacity dark:border-white/10 dark:bg-white/[0.04]"
style={{
opacity: on ? 1 : 0,
transform: on ? "none" : `translateY(${i < plan ? -6 : 6}px)`,
transitionDelay: on ? "120ms" : "0ms",
}}
>
<div className="flex items-baseline gap-1.5">
<span className="text-4xl font-semibold tracking-tight tabular-nums">
${p.price}
</span>
<span className="text-sm text-[#0b0b0d]/55 dark:text-[#f4f4f6]/55">
a month
</span>
</div>
<p className="mt-1 text-sm text-[#0b0b0d]/65 dark:text-[#f4f4f6]/65">
{p.blurb}
</p>
<ul className="mt-4 space-y-2 text-sm">
{p.lines.map((line) => (
<li key={line} className="flex gap-2">
<Check />
{line}
</li>
))}
</ul>
<a
href="#plan"
tabIndex={on ? undefined : -1}
className="mt-5 flex h-10 items-center justify-center rounded-full bg-[#ff3d1f] text-sm font-semibold text-white transition-colors duration-150 hover:bg-[#e8331a] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#ff3d1f] dark:bg-[#ff5a3a] dark:text-[#0b0b0d] dark:hover:bg-[#ff6e52]"
>
{p.cta}
</a>
</div>
);
})}
</div>
</div>
</section>
);
}
Live preview, examples and props: https://keyframeui.com/components/echo-reel#echo-reel-plans-demo