Calculator Pricing
Original · freeinteractive pricing calculator
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/calc-pricing.json"use client";
import { useEffect, useId, useState } from "react";
import {
AnimatePresence,
animate,
motion,
useMotionValue,
useReducedMotion,
useTransform,
} from "motion/react";
type ProjectKey = "landing" | "marketing" | "store" | "app";
type AddOnKey = "copy" | "seo" | "illustration" | "motion" | "analytics";
type SpeedKey = "standard" | "priority" | "rush";
type Project = {
name: string;
blurb: string;
base: number;
pages: number;
weeks: number;
};
type AddOn = {
name: string;
note: string;
price: number;
};
type Speed = {
name: string;
note: string;
multiplier: number;
timeFactor: number;
};
type Row = {
id: string;
label: string;
detail: string;
amount: number;
};
const PAGE_RATE = 320;
const CARE_RATE = 180;
const DEPOSIT_SHARE = 0.4;
const MAX_EXTRA_PAGES = 16;
const PROJECT_ORDER: ProjectKey[] = ["landing", "marketing", "store", "app"];
const PROJECTS: Record<ProjectKey, Project> = {
landing: {
name: "Landing page",
blurb: "One page, one conversion goal, built to be measured.",
base: 1400,
pages: 1,
weeks: 2,
},
marketing: {
name: "Marketing site",
blurb: "Multi-page site with a blog and a CMS your team can drive.",
base: 3800,
pages: 5,
weeks: 4,
},
store: {
name: "Online store",
blurb: "Catalogue, cart and checkout wired to your payment provider.",
base: 6500,
pages: 8,
weeks: 7,
},
app: {
name: "Web app MVP",
blurb: "Auth, dashboard and the first honest slice of the product.",
base: 12000,
pages: 10,
weeks: 11,
},
};
const ADDON_ORDER: AddOnKey[] = ["copy", "seo", "illustration", "motion", "analytics"];
const ADDONS: Record<AddOnKey, AddOn> = {
copy: {
name: "Copywriting",
note: "Two research interviews, then every word written for you.",
price: 850,
},
seo: {
name: "SEO foundation",
note: "Schema, sitemaps, metadata and a Core Web Vitals pass.",
price: 600,
},
illustration: {
name: "Illustration set",
note: "Six custom vector scenes drawn around your brand.",
price: 1100,
},
motion: {
name: "Motion & interaction",
note: "Scroll choreography and micro-interactions, reduced-motion safe.",
price: 750,
},
analytics: {
name: "Analytics wiring",
note: "Events, funnels and a dashboard you will actually open.",
price: 380,
},
};
const SPEED_ORDER: SpeedKey[] = ["standard", "priority", "rush"];
const SPEEDS: Record<SpeedKey, Speed> = {
standard: { name: "Standard", note: "Normal queue, weekly check-ins", multiplier: 1, timeFactor: 1 },
priority: { name: "Priority", note: "Queue jump, reviews twice a week", multiplier: 1.25, timeFactor: 0.75 },
rush: { name: "Rush", note: "Dedicated sprint, daily stand-ups", multiplier: 1.5, timeFactor: 0.55 },
};
const gbp = (value: number): string => `£${Math.round(value).toLocaleString("en-GB")}`;
const panelClass =
"rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm sm:p-6 dark:border-zinc-800 dark:bg-zinc-900/50";
const legendClass =
"flex items-center gap-2 text-sm font-semibold tracking-tight text-zinc-900 dark:text-zinc-100";
const stepClass =
"grid h-5 w-5 place-items-center rounded-md bg-zinc-900 text-[10px] font-bold text-white tabular-nums dark:bg-zinc-100 dark:text-zinc-900";
const focusRing =
"peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-indigo-600 dark:peer-focus-visible:outline-indigo-400";
function CheckIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path
d="M4.5 10.5 8 14l7.5-8"
stroke="currentColor"
strokeWidth="2.4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function AnimatedAmount({
value,
motionOn,
className,
}: {
value: number;
motionOn: boolean;
className?: string;
}) {
const raw = useMotionValue(value);
const text = useTransform(raw, (current: number) => Math.round(current).toLocaleString("en-GB"));
useEffect(() => {
if (!motionOn) {
raw.set(value);
return;
}
const controls = animate(raw, value, { duration: 0.45, ease: "easeOut" });
return () => controls.stop();
}, [value, motionOn, raw]);
return (
<span className={className} aria-hidden="true">
£<motion.span>{text}</motion.span>
</span>
);
}
export default function CalcPricing() {
const uid = useId();
const reduce = useReducedMotion();
const motionOn = !reduce;
const [projectKey, setProjectKey] = useState<ProjectKey>("marketing");
const [extraPages, setExtraPages] = useState<number>(3);
const [selected, setSelected] = useState<Record<AddOnKey, boolean>>({
copy: false,
seo: true,
illustration: false,
motion: false,
analytics: false,
});
const [speedKey, setSpeedKey] = useState<SpeedKey>("standard");
const [care, setCare] = useState<boolean>(true);
const [copied, setCopied] = useState<boolean>(false);
const project = PROJECTS[projectKey];
const speed = SPEEDS[speedKey];
const addOnKeys = ADDON_ORDER.filter((key) => selected[key]);
const addOnsTotal = addOnKeys.reduce((sum, key) => sum + ADDONS[key].price, 0);
const pagesTotal = extraPages * PAGE_RATE;
const subtotal = project.base + pagesTotal + addOnsTotal;
const uplift = Math.round(subtotal * (speed.multiplier - 1));
const total = subtotal + uplift;
const deposit = Math.round(total * DEPOSIT_SHARE);
const weeks = Math.max(
1,
Math.round((project.weeks + extraPages / 5 + addOnKeys.length * 0.5) * speed.timeFactor),
);
const rows: Row[] = [
{
id: "base",
label: project.name,
detail: `${project.pages} page${project.pages === 1 ? "" : "s"} included`,
amount: project.base,
},
];
if (extraPages > 0) {
rows.push({
id: "pages",
label: "Extra pages",
detail: `${extraPages} × ${gbp(PAGE_RATE)}`,
amount: pagesTotal,
});
}
for (const key of addOnKeys) {
rows.push({ id: key, label: ADDONS[key].name, detail: "Add-on", amount: ADDONS[key].price });
}
if (uplift > 0) {
rows.push({
id: "speed",
label: `${speed.name} delivery`,
detail: `+${Math.round((speed.multiplier - 1) * 100)}% of ${gbp(subtotal)}`,
amount: uplift,
});
}
useEffect(() => {
if (!copied) return;
const timer = window.setTimeout(() => setCopied(false), 2200);
return () => window.clearTimeout(timer);
}, [copied]);
const toggleAddOn = (key: AddOnKey) => {
setSelected((previous) => ({ ...previous, [key]: !previous[key] }));
};
const reset = () => {
setProjectKey("marketing");
setExtraPages(3);
setSelected({ copy: false, seo: true, illustration: false, motion: false, analytics: false });
setSpeedKey("standard");
setCare(true);
};
const handleCopy = async () => {
const lines = [
"Project estimate",
...rows.map((row) => `${row.label} (${row.detail}) — ${gbp(row.amount)}`),
`Total — ${gbp(total)} + VAT`,
`Deposit to start (40%) — ${gbp(deposit)}`,
`Estimated timeline — about ${weeks} week${weeks === 1 ? "" : "s"}`,
care ? `Care plan — ${gbp(CARE_RATE)} per month` : "Care plan — not included",
];
try {
await navigator.clipboard.writeText(lines.join("\n"));
setCopied(true);
} catch {
setCopied(false);
}
};
const sliderPercent = (extraPages / MAX_EXTRA_PAGES) * 100;
return (
<section className="relative w-full overflow-hidden bg-white px-5 py-20 text-zinc-900 sm:px-8 md:py-28 dark:bg-zinc-950 dark:text-zinc-50">
<style>{`
@keyframes cpx-sheen {
0% { transform: translateX(-120%); }
100% { transform: translateX(420%); }
}
@keyframes cpx-pulse {
0%, 100% { opacity: 0.35; transform: scale(1); }
50% { opacity: 1; transform: scale(1.6); }
}
.cpx-sheen { animation: cpx-sheen 4.2s ease-in-out infinite; }
.cpx-pulse { animation: cpx-pulse 2.4s ease-in-out infinite; }
.cpx-range {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 8px;
border-radius: 9999px;
background-image: linear-gradient(currentColor, currentColor);
background-repeat: no-repeat;
cursor: pointer;
}
.cpx-range::-webkit-slider-runnable-track {
height: 8px;
border-radius: 9999px;
background: transparent;
}
.cpx-range::-moz-range-track {
height: 8px;
border-radius: 9999px;
background: transparent;
}
.cpx-range::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
margin-top: -7px;
height: 22px;
width: 22px;
border-radius: 9999px;
background: #ffffff;
border: 2px solid currentColor;
box-shadow: 0 1px 4px rgba(9, 9, 11, 0.3);
transition: transform 0.16s ease;
}
.cpx-range::-moz-range-thumb {
height: 22px;
width: 22px;
border-radius: 9999px;
background: #ffffff;
border: 2px solid currentColor;
box-shadow: 0 1px 4px rgba(9, 9, 11, 0.3);
transition: transform 0.16s ease;
}
.cpx-range:active::-webkit-slider-thumb { transform: scale(1.14); }
.cpx-range:active::-moz-range-thumb { transform: scale(1.14); }
@media (prefers-reduced-motion: reduce) {
.cpx-sheen, .cpx-pulse { animation: none !important; }
.cpx-range::-webkit-slider-thumb { transition: none !important; }
.cpx-range::-moz-range-thumb { transition: none !important; }
}
`}</style>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-[radial-gradient(55rem_32rem_at_50%_-8%,rgba(99,102,241,0.12),transparent_70%)] dark:bg-[radial-gradient(55rem_32rem_at_50%_-8%,rgba(129,140,248,0.16),transparent_70%)]"
/>
<div className="relative mx-auto max-w-6xl">
<header className="max-w-2xl">
<p className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white/70 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-zinc-600 backdrop-blur dark:border-zinc-800 dark:bg-zinc-900/70 dark:text-zinc-400">
<span className="relative grid h-2 w-2 place-items-center">
<span className="cpx-pulse absolute h-1.5 w-1.5 rounded-full bg-emerald-500" />
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
</span>
Live estimate
</p>
<h2 className="mt-5 text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl dark:text-white">
Work out the budget before you book the call
</h2>
<p className="mt-4 text-base leading-relaxed text-zinc-600 sm:text-lg dark:text-zinc-400">
Pick a starting point, add what you actually need, and watch the number move. Nothing is
hidden behind a form — copy the breakdown and take it to your finance team.
</p>
</header>
<div className="mt-12 grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_23rem]">
<div className="space-y-6">
<fieldset className={panelClass}>
<legend className={legendClass}>
<span className={stepClass}>1</span>
What are we building?
</legend>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
{PROJECT_ORDER.map((key) => {
const item = PROJECTS[key];
return (
<label key={key} className="group relative block cursor-pointer">
<input
type="radio"
name={`${uid}-project`}
value={key}
checked={projectKey === key}
onChange={() => setProjectKey(key)}
className="peer sr-only"
/>
<span
className={`flex h-full flex-col rounded-xl border border-zinc-200 bg-zinc-50/60 p-4 transition-colors peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:border-zinc-800 dark:bg-zinc-950/40 dark:peer-checked:border-indigo-400 dark:peer-checked:bg-indigo-500/10 ${focusRing}`}
>
<span className="flex items-center justify-between gap-3">
<span className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
{item.name}
</span>
<span className="grid h-4 w-4 shrink-0 place-items-center rounded-full border-2 border-zinc-300 transition-colors group-has-[:checked]:border-indigo-600 dark:border-zinc-600 dark:group-has-[:checked]:border-indigo-400">
<span className="h-1.5 w-1.5 scale-0 rounded-full bg-indigo-600 transition-transform group-has-[:checked]:scale-100 dark:bg-indigo-400" />
</span>
</span>
<span className="mt-1.5 text-xs leading-relaxed text-zinc-500 dark:text-zinc-400">
{item.blurb}
</span>
<span className="mt-3 flex items-baseline gap-1.5 text-xs text-zinc-500 dark:text-zinc-400">
<span className="text-sm font-semibold tabular-nums text-zinc-900 dark:text-zinc-100">
from {gbp(item.base)}
</span>
<span aria-hidden="true">·</span>
<span>~{item.weeks} weeks</span>
</span>
</span>
</label>
);
})}
</div>
</fieldset>
<div className={panelClass}>
<div className="flex flex-wrap items-center justify-between gap-3">
<label
htmlFor={`${uid}-pages`}
className="flex items-center gap-2 text-sm font-semibold tracking-tight text-zinc-900 dark:text-zinc-100"
>
<span className={stepClass}>2</span>
Extra pages
</label>
<output
htmlFor={`${uid}-pages`}
className="rounded-lg bg-zinc-100 px-2.5 py-1 text-sm font-semibold tabular-nums text-zinc-900 dark:bg-zinc-800 dark:text-zinc-100"
>
{extraPages} × {gbp(PAGE_RATE)}
</output>
</div>
<p id={`${uid}-pages-help`} className="mt-2 text-xs text-zinc-500 dark:text-zinc-400">
{project.name} includes {project.pages} page{project.pages === 1 ? "" : "s"}. Add more
for anything beyond that — each one is designed, written into the CMS and tested.
</p>
<input
id={`${uid}-pages`}
type="range"
min={0}
max={MAX_EXTRA_PAGES}
step={1}
value={extraPages}
onChange={(event) => setExtraPages(Number(event.target.value))}
aria-describedby={`${uid}-pages-help`}
aria-valuetext={`${extraPages} extra pages, ${gbp(pagesTotal)}`}
style={{ backgroundSize: `${sliderPercent}% 100%` }}
className="cpx-range mt-5 bg-zinc-200 text-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-indigo-600 dark:bg-zinc-800 dark:text-indigo-400 dark:focus-visible:outline-indigo-400"
/>
<div className="mt-2 flex justify-between text-[11px] font-medium tabular-nums text-zinc-400 dark:text-zinc-500">
<span>0</span>
<span>8</span>
<span>{MAX_EXTRA_PAGES}</span>
</div>
</div>
<fieldset className={panelClass}>
<legend className={legendClass}>
<span className={stepClass}>3</span>
Add-ons
</legend>
<div className="mt-4 space-y-2.5">
{ADDON_ORDER.map((key) => {
const item = ADDONS[key];
return (
<label key={key} className="group relative block cursor-pointer">
<input
type="checkbox"
checked={selected[key]}
onChange={() => toggleAddOn(key)}
className="peer sr-only"
/>
<span
className={`flex items-start gap-3 rounded-xl border border-zinc-200 bg-zinc-50/60 p-4 transition-colors peer-checked:border-violet-500 peer-checked:bg-violet-50 dark:border-zinc-800 dark:bg-zinc-950/40 dark:peer-checked:border-violet-400 dark:peer-checked:bg-violet-500/10 ${focusRing}`}
>
<span className="mt-0.5 grid h-5 w-5 shrink-0 place-items-center rounded-md border-2 border-zinc-300 text-white transition-colors group-has-[:checked]:border-violet-600 group-has-[:checked]:bg-violet-600 dark:border-zinc-600 dark:group-has-[:checked]:border-violet-500 dark:group-has-[:checked]:bg-violet-500">
<CheckIcon className="h-3.5 w-3.5 scale-0 transition-transform group-has-[:checked]:scale-100" />
</span>
<span className="min-w-0 flex-1">
<span className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
<span className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
{item.name}
</span>
<span className="text-sm font-semibold tabular-nums text-zinc-900 dark:text-zinc-100">
+{gbp(item.price)}
</span>
</span>
<span className="mt-1 block text-xs leading-relaxed text-zinc-500 dark:text-zinc-400">
{item.note}
</span>
</span>
</span>
</label>
);
})}
</div>
</fieldset>
<fieldset className={panelClass}>
<legend className={legendClass}>
<span className={stepClass}>4</span>
How fast do you need it?
</legend>
<div className="mt-4 grid gap-2.5 sm:grid-cols-3">
{SPEED_ORDER.map((key) => {
const item = SPEEDS[key];
return (
<label key={key} className="group relative block cursor-pointer">
<input
type="radio"
name={`${uid}-speed`}
value={key}
checked={speedKey === key}
onChange={() => setSpeedKey(key)}
className="peer sr-only"
/>
<span
className={`flex h-full flex-col rounded-xl border border-zinc-200 bg-zinc-50/60 px-4 py-3 text-center transition-colors peer-checked:border-zinc-900 peer-checked:bg-zinc-900 dark:border-zinc-800 dark:bg-zinc-950/40 dark:peer-checked:border-zinc-100 dark:peer-checked:bg-zinc-100 ${focusRing}`}
>
<span className="text-sm font-semibold text-zinc-900 group-has-[:checked]:text-white dark:text-zinc-100 dark:group-has-[:checked]:text-zinc-900">
{item.name}
{item.multiplier > 1 ? (
<span className="ml-1 text-amber-600 group-has-[:checked]:text-amber-300 dark:text-amber-400 dark:group-has-[:checked]:text-amber-600">
+{Math.round((item.multiplier - 1) * 100)}%
</span>
) : null}
</span>
<span className="mt-1 text-[11px] leading-snug text-zinc-500 group-has-[:checked]:text-zinc-300 dark:text-zinc-400 dark:group-has-[:checked]:text-zinc-600">
{item.note}
</span>
</span>
</label>
);
})}
</div>
<label className="group mt-4 flex cursor-pointer items-center justify-between gap-4 rounded-xl border border-zinc-200 bg-zinc-50/60 p-4 dark:border-zinc-800 dark:bg-zinc-950/40">
<span className="min-w-0">
<span className="block text-sm font-semibold text-zinc-900 dark:text-zinc-100">
Care plan — {gbp(CARE_RATE)}/month
</span>
<span className="mt-1 block text-xs leading-relaxed text-zinc-500 dark:text-zinc-400">
Hosting, security updates, uptime alerts and four hours of changes each month.
</span>
</span>
<input
type="checkbox"
role="switch"
checked={care}
onChange={() => setCare((previous) => !previous)}
aria-label={`Care plan at ${gbp(CARE_RATE)} per month`}
className="peer sr-only"
/>
<span
className={`relative h-6 w-11 shrink-0 rounded-full bg-zinc-300 transition-colors peer-checked:bg-emerald-600 dark:bg-zinc-700 dark:peer-checked:bg-emerald-500 ${focusRing}`}
>
<span className="absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform group-has-[:checked]:translate-x-5" />
</span>
</label>
</fieldset>
</div>
<aside className="lg:sticky lg:top-6">
<div className="relative overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-xl shadow-zinc-900/5 dark:border-zinc-800 dark:bg-zinc-900">
<div aria-hidden="true" className="pointer-events-none absolute inset-x-0 top-0 h-px overflow-hidden">
<div className="cpx-sheen h-px w-1/4 bg-gradient-to-r from-transparent via-indigo-500 to-transparent dark:via-indigo-400" />
</div>
<div className="p-6">
<div className="flex items-center justify-between gap-3">
<h3 className="text-sm font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
Your estimate
</h3>
<button
type="button"
onClick={reset}
className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-xs font-medium text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100 dark:focus-visible:outline-indigo-400"
>
<svg className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path
d="M15.5 6.5A6 6 0 1 0 16 10M15.5 3v4h-4"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
Reset
</button>
</div>
<ul className="mt-4 space-y-0 text-sm">
<AnimatePresence initial={false}>
{rows.map((row) => (
<motion.li
key={row.id}
layout={motionOn}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: motionOn ? 0.24 : 0, ease: "easeOut" }}
className="overflow-hidden"
>
<span className="flex items-baseline justify-between gap-4 border-b border-dashed border-zinc-200 py-2.5 dark:border-zinc-800">
<span className="min-w-0">
<span className="block truncate font-medium text-zinc-800 dark:text-zinc-200">
{row.label}
</span>
<span className="block truncate text-xs text-zinc-500 dark:text-zinc-400">
{row.detail}
</span>
</span>
<span className="shrink-0 font-semibold tabular-nums text-zinc-900 dark:text-zinc-100">
{gbp(row.amount)}
</span>
</span>
</motion.li>
))}
</AnimatePresence>
</ul>
<div className="mt-5 flex items-end justify-between gap-4">
<div>
<p className="text-xs font-medium uppercase tracking-wider text-zinc-500 dark:text-zinc-400">
Total
</p>
<AnimatedAmount
value={total}
motionOn={motionOn}
className="mt-1 block text-4xl font-bold tracking-tight tabular-nums text-zinc-900 dark:text-white"
/>
</div>
<p className="pb-1.5 text-xs text-zinc-500 dark:text-zinc-400">+ VAT</p>
</div>
<p className="sr-only" aria-live="polite">
Estimated total {gbp(total)} plus VAT over about {weeks} week
{weeks === 1 ? "" : "s"}. Deposit to start {gbp(deposit)}.
{care ? ` Care plan ${gbp(CARE_RATE)} per month.` : " Care plan not included."}
</p>
<dl className="mt-5 grid grid-cols-2 gap-2.5 text-xs">
<div className="rounded-xl bg-zinc-50 p-3 dark:bg-zinc-800/60">
<dt className="text-zinc-500 dark:text-zinc-400">40% to start</dt>
<dd className="mt-1 text-base font-semibold tabular-nums text-zinc-900 dark:text-zinc-100">
{gbp(deposit)}
</dd>
</div>
<div className="rounded-xl bg-zinc-50 p-3 dark:bg-zinc-800/60">
<dt className="text-zinc-500 dark:text-zinc-400">Timeline</dt>
<dd className="mt-1 text-base font-semibold tabular-nums text-zinc-900 dark:text-zinc-100">
~{weeks} week{weeks === 1 ? "" : "s"}
</dd>
</div>
</dl>
<AnimatePresence initial={false}>
{care ? (
<motion.div
layout={motionOn}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: motionOn ? 0.24 : 0, ease: "easeOut" }}
className="overflow-hidden"
>
<p className="mt-2.5 flex items-center justify-between gap-3 rounded-xl bg-emerald-50 p-3 text-xs font-medium text-emerald-800 dark:bg-emerald-500/10 dark:text-emerald-300">
<span className="inline-flex items-center gap-1.5">
<CheckIcon className="h-3.5 w-3.5" />
Care plan, billed after launch
</span>
<span className="tabular-nums">{gbp(CARE_RATE)}/mo</span>
</p>
</motion.div>
) : null}
</AnimatePresence>
<a
href="#"
className="mt-5 flex w-full items-center justify-center gap-2 rounded-xl bg-zinc-900 px-4 py-3 text-sm font-semibold text-white transition-colors hover:bg-zinc-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200 dark:focus-visible:outline-indigo-400"
>
Book a 30-minute scoping call
<svg className="h-4 w-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path
d="M4 10h12m-5-5 5 5-5 5"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</a>
<button
type="button"
onClick={handleCopy}
className="mt-2.5 flex w-full items-center justify-center gap-2 rounded-xl border border-zinc-200 px-4 py-2.5 text-sm font-semibold text-zinc-700 transition-colors hover:bg-zinc-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800 dark:focus-visible:outline-indigo-400"
>
{copied ? (
<CheckIcon className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
) : (
<svg className="h-4 w-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<rect
x="7"
y="7"
width="9"
height="9"
rx="2"
stroke="currentColor"
strokeWidth="1.6"
/>
<path
d="M13 7V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h1"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
/>
</svg>
)}
{copied ? "Breakdown copied" : "Copy the breakdown"}
</button>
<span className="sr-only" aria-live="polite">
{copied ? "Estimate breakdown copied to your clipboard" : ""}
</span>
</div>
<p className="border-t border-zinc-100 bg-zinc-50/70 px-6 py-4 text-[11px] leading-relaxed text-zinc-500 dark:border-zinc-800 dark:bg-zinc-950/40 dark:text-zinc-400">
An estimate, not a quote. Final numbers land once we have seen your content,
integrations and deadline — usually within 5% of what you see here.
</p>
</div>
</aside>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Calculator Basic
Originalbasic calculator keypad UI

Calculator Loan
Originalloan / mortgage calculator with sliders

Calculator Tip
Originaltip and split calculator

Calculator ROI
OriginalROI / savings calculator

Calculator Unit Converter
Originalunit converter

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero
OriginalA centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero
OriginalA dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

Image Backdrop Hero
OriginalA cinematic full-bleed hero with a layered image-style backdrop, overlaid left-aligned copy and a trusted-by logos strip.

Gradient Mesh Hero with Staggered Text Reveal
OriginalA full-screen hero pairing a drifting animated gradient-mesh backdrop with a word-by-word staggered blur-in headline and a logo marquee under the fold.

