Three plans as radio cards. The note swings its arrow to whichever plan you pick and rewrites itself for it.
registry/demos/jot-plans-demo.tsx
"use client";
import * as React from "react";
import { Jot } from "@/components/ui/jot";
const PLANS = [
{
id: "solo",
name: "Solo",
price: 0,
blurb: "One client, every file",
terms: "Free for good, no card needed",
note: "free forever, no card needed",
},
{
id: "studio",
name: "Studio",
price: 24,
blurb: "Ten clients, your logo",
terms: "14 days free, then $24 a month",
note: "most studios start here",
},
{
id: "agency",
name: "Agency",
price: 79,
blurb: "Every client, with SSO",
terms: "14 days free, then $79 a month",
note: "for when you outgrow ten",
},
] as const;
type PlanId = (typeof PLANS)[number]["id"];
export function JotPlansDemo() {
const name = React.useId();
const [picked, setPicked] = React.useState<PlanId>("studio");
const plan = PLANS.find((p) => p.id === picked) ?? PLANS[1];
const index = PLANS.indexOf(plan);
return (
<div data-jot-scope className="@container/plans relative w-full max-w-[38rem] px-2 pt-24 pb-4">
<fieldset>
<legend className="text-[13px] font-medium text-zinc-500 dark:text-zinc-400">Lantern plans</legend>
<p className="mt-1 max-w-[16ch] text-xl/tight font-semibold tracking-[-0.02em] text-balance text-zinc-950 dark:text-white">
Client portals for small studios
</p>
<div className="mt-6 grid grid-cols-3 gap-2 @md/plans:gap-3">
{PLANS.map((p) => (
<label
key={p.id}
data-plan={p.id}
className="group relative flex cursor-pointer flex-col rounded-2xl border border-zinc-950/10 bg-white p-3 transition-[border-color,box-shadow] duration-200 ease-out hover:border-zinc-950/25 has-checked:border-zinc-950 has-checked:shadow-[0_0_0_1px_var(--color-zinc-950)] has-focus-visible:ring-2 has-focus-visible:ring-blue-500 has-focus-visible:ring-offset-2 @md/plans:p-4 dark:border-white/10 dark:bg-zinc-900 dark:hover:border-white/25 dark:has-checked:border-white dark:has-checked:shadow-[0_0_0_1px_var(--color-white)] dark:has-focus-visible:ring-offset-zinc-950"
>
<input
type="radio"
name={name}
value={p.id}
checked={picked === p.id}
onChange={() => setPicked(p.id)}
className="sr-only"
/>
<span className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-zinc-950 dark:text-white">{p.name}</span>
<span
aria-hidden
className="grid size-4 place-items-center rounded-full border border-zinc-950/20 transition-colors duration-150 group-has-checked:border-zinc-950 dark:border-white/25 dark:group-has-checked:border-white"
>
<span className="size-2 scale-50 rounded-full bg-zinc-950 opacity-0 transition-[scale,opacity] duration-150 ease-out group-has-checked:scale-100 group-has-checked:opacity-100 dark:bg-white" />
</span>
</span>
<span className="mt-4 flex items-baseline gap-0.5 text-zinc-950 dark:text-white">
<span className="text-2xl/none font-semibold tracking-tight tabular-nums @md/plans:text-[1.75rem]/none">
${p.price}
</span>
<span className="text-xs text-zinc-500 dark:text-zinc-400">/mo</span>
</span>
<span className="mt-2 text-xs/4 text-pretty text-zinc-500 dark:text-zinc-400">{p.blurb}</span>
</label>
))}
</div>
</fieldset>
<div className="mt-3 flex flex-wrap items-center justify-between gap-x-4 gap-y-3 rounded-2xl bg-zinc-100/80 py-2.5 pr-2.5 pl-4 dark:bg-white/5">
<p className="text-sm text-zinc-600 dark:text-zinc-400" aria-live="polite">
{plan.terms}
</p>
<a
href="#start"
className="inline-flex h-9 items-center rounded-full bg-zinc-950 px-4 text-sm font-medium text-white outline-none hover:bg-zinc-800 focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 dark:bg-white dark:text-zinc-950 dark:hover:bg-zinc-200 dark:focus-visible:ring-white dark:focus-visible:ring-offset-zinc-950"
>
Start with {plan.name}
</a>
</div>
<Jot
to={`[data-plan="${picked}"]`}
at="top"
// Leave sideways and land heading down: bow up toward whichever side the plan is on.
bend={index === 2 ? -0.45 : 0.45}
tilt={-2}
trigger="mount"
delay={0.3}
className="absolute top-4 right-[8%] text-xl/snug text-blue-600 @md/plans:right-[14%] dark:text-blue-400"
>
{plan.note}
</Jot>
</div>
);
}
Live preview, examples and props: https://keyframeui.com/components/jot#jot-plans-demo
