Web InnoventixFreeCode

Addons Pricing

Original · free

pricing with selectable add-ons

byWeb InnoventixReact + Tailwind
pricexaddonspricing
Open

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/pricex-addons.json
pricex-addons.tsx
"use client";

import { useEffect, useId, useMemo, useState, type CSSProperties } from "react";
import {
  motion,
  useReducedMotion,
  useMotionValue,
  useTransform,
  animate,
  type MotionProps,
} from "motion/react";

type Plan = {
  id: string;
  name: string;
  monthly: number;
  tagline: string;
  features: string[];
  popular?: boolean;
};

type AddOn = {
  id: string;
  name: string;
  desc: string;
  monthly: number;
};

const PLANS: Plan[] = [
  {
    id: "starter",
    name: "Starter",
    monthly: 19,
    tagline: "For solo makers shipping their first real product.",
    features: [
      "1 workspace",
      "Up to 3 active projects",
      "5 GB asset storage",
      "Community support",
    ],
  },
  {
    id: "growth",
    name: "Growth",
    monthly: 49,
    tagline: "For growing teams that need room to move fast.",
    features: [
      "5 workspaces",
      "Unlimited projects",
      "100 GB asset storage",
      "Automation workflows",
      "Email support",
    ],
    popular: true,
  },
  {
    id: "scale",
    name: "Scale",
    monthly: 99,
    tagline: "For organizations that need control at scale.",
    features: [
      "Unlimited workspaces",
      "SSO & audit logs",
      "1 TB asset storage",
      "Advanced permissions",
      "24/7 priority support",
    ],
  },
];

const ADDONS: AddOn[] = [
  {
    id: "priority",
    name: "Priority support",
    desc: "Skip the queue with a 2-hour response SLA.",
    monthly: 12,
  },
  {
    id: "analytics",
    name: "Advanced analytics",
    desc: "Cohort, funnel, and retention dashboards.",
    monthly: 18,
  },
  {
    id: "seats",
    name: "Extra seats (×5)",
    desc: "Add five more collaborators to any plan.",
    monthly: 25,
  },
  {
    id: "domains",
    name: "Custom domains",
    desc: "Serve projects from your own branded domain.",
    monthly: 8,
  },
  {
    id: "onboarding",
    name: "Onboarding & migration",
    desc: "White-glove setup with a dedicated specialist.",
    monthly: 40,
  },
];

const ANNUAL_FACTOR = 0.8; // 20% off, two months free

function CheckIcon({
  className,
  style,
}: {
  className?: string;
  style?: CSSProperties;
}) {
  return (
    <svg
      viewBox="0 0 20 20"
      fill="none"
      aria-hidden="true"
      className={className}
      style={style}
    >
      <path
        d="M4 10.5 8 14.5 16 5.5"
        stroke="currentColor"
        strokeWidth="2.2"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function ArrowIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 20 20"
      fill="none"
      aria-hidden="true"
      className={className}
    >
      <path
        d="M4 10h11M11 5l5 5-5 5"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function money(n: number): string {
  return "$" + Math.round(n).toLocaleString("en-US");
}

export default function PricexAddons() {
  const reduce = useReducedMotion();
  const groupName = useId();

  const [planId, setPlanId] = useState<string>("growth");
  const [billing, setBilling] = useState<"monthly" | "annual">("annual");
  const [addOns, setAddOns] = useState<string[]>(["analytics"]);

  const plan = useMemo(
    () => PLANS.find((p) => p.id === planId) ?? PLANS[0],
    [planId]
  );

  const addOnsMonthly = useMemo(
    () =>
      ADDONS.filter((a) => addOns.includes(a.id)).reduce(
        (sum, a) => sum + a.monthly,
        0
      ),
    [addOns]
  );

  const subtotal = plan.monthly + addOnsMonthly;
  const factor = billing === "annual" ? ANNUAL_FACTOR : 1;
  const perMonth = subtotal * factor;
  const billedYearly = perMonth * 12;
  const yearlySavings = subtotal * 12 * (1 - ANNUAL_FACTOR);

  const price = useMotionValue(perMonth);
  const priceText = useTransform(price, (v) => money(v));

  useEffect(() => {
    const controls = animate(price, perMonth, {
      duration: reduce ? 0 : 0.45,
      ease: "easeOut",
    });
    return () => controls.stop();
  }, [perMonth, price, reduce]);

  function toggleAddOn(id: string) {
    setAddOns((prev) =>
      prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
    );
  }

  const fade: MotionProps = reduce
    ? {}
    : {
        initial: { opacity: 0, y: 16 },
        whileInView: { opacity: 1, y: 0 },
        viewport: { once: true, margin: "-80px" },
        transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] },
      };

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 via-white to-slate-100 px-5 py-20 text-slate-900 sm:px-8 sm:py-28 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 dark:text-slate-100">
      <style>{`
        @keyframes pxaddon-blob {
          0%, 100% { transform: translate(0, 0) scale(1); }
          33% { transform: translate(28px, -22px) scale(1.08); }
          66% { transform: translate(-22px, 18px) scale(0.94); }
        }
        @keyframes pxaddon-sheen {
          0% { background-position: -180% 0; }
          100% { background-position: 180% 0; }
        }
        @keyframes pxaddon-pop {
          0% { transform: scale(0.4); opacity: 0; }
          60% { transform: scale(1.15); }
          100% { transform: scale(1); opacity: 1; }
        }
        @media (prefers-reduced-motion: reduce) {
          .pxaddon-anim { animation: none !important; }
        }
      `}</style>

      {/* Decorative background */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0">
        <div
          className="pxaddon-anim absolute -left-24 top-8 h-72 w-72 rounded-full bg-indigo-400/25 blur-3xl dark:bg-indigo-500/20"
          style={{ animation: "pxaddon-blob 20s ease-in-out infinite" }}
        />
        <div
          className="pxaddon-anim absolute -right-20 top-40 h-80 w-80 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-500/20"
          style={{ animation: "pxaddon-blob 26s ease-in-out infinite reverse" }}
        />
        <div
          className="absolute inset-0 opacity-[0.35] dark:opacity-20"
          style={{
            backgroundImage:
              "linear-gradient(to right, rgba(100,116,139,0.12) 1px, transparent 1px), linear-gradient(to bottom, rgba(100,116,139,0.12) 1px, transparent 1px)",
            backgroundSize: "44px 44px",
            maskImage:
              "radial-gradient(ellipse 80% 60% at 50% 0%, #000 40%, transparent 100%)",
            WebkitMaskImage:
              "radial-gradient(ellipse 80% 60% at 50% 0%, #000 40%, transparent 100%)",
          }}
        />
      </div>

      <div className="relative mx-auto max-w-6xl">
        {/* Header */}
        <motion.div {...fade} className="mx-auto max-w-2xl text-center">
          <span className="inline-flex items-center gap-2 rounded-full border border-indigo-200/70 bg-white/70 px-3.5 py-1.5 text-xs font-semibold uppercase tracking-[0.14em] text-indigo-700 backdrop-blur dark:border-indigo-400/20 dark:bg-white/5 dark:text-indigo-300">
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
            Build your plan
          </span>
          <h2 className="mt-6 text-balance text-4xl font-semibold tracking-tight sm:text-5xl">
            Pay for the core.
            <span className="bg-gradient-to-r from-indigo-600 via-violet-600 to-indigo-500 bg-clip-text text-transparent dark:from-indigo-400 dark:via-violet-400 dark:to-indigo-300">
              {" "}
              Add only what you need.
            </span>
          </h2>
          <p className="mt-4 text-pretty text-base leading-relaxed text-slate-600 sm:text-lg dark:text-slate-400">
            Start with a plan that fits your team, then bolt on the extras that
            matter. Your total updates live — no surprises at checkout.
          </p>
        </motion.div>

        {/* Billing toggle */}
        <motion.div
          {...fade}
          className="mt-10 flex items-center justify-center"
        >
          <div
            role="radiogroup"
            aria-label="Billing period"
            className="relative inline-flex rounded-full border border-slate-200 bg-white/80 p-1 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5"
          >
            {(["monthly", "annual"] as const).map((mode) => {
              const active = billing === mode;
              return (
                <button
                  key={mode}
                  role="radio"
                  aria-checked={active}
                  onClick={() => setBilling(mode)}
                  className={`relative z-10 rounded-full px-5 py-2 text-sm font-semibold capitalize transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 ${
                    active
                      ? "text-white"
                      : "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-100"
                  }`}
                >
                  {active && (
                    <motion.span
                      layoutId={reduce ? undefined : `${groupName}-pill`}
                      transition={{ type: "spring", stiffness: 400, damping: 32 }}
                      className="absolute inset-0 -z-10 rounded-full bg-gradient-to-r from-indigo-600 to-violet-600 shadow-lg shadow-indigo-500/30"
                    />
                  )}
                  {mode}
                  {mode === "annual" && (
                    <span
                      className={`ml-2 rounded-full px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide ${
                        active
                          ? "bg-white/25 text-white"
                          : "bg-emerald-100 text-emerald-700 dark:bg-emerald-400/15 dark:text-emerald-300"
                      }`}
                    >
                      −20%
                    </span>
                  )}
                </button>
              );
            })}
          </div>
        </motion.div>

        {/* Main grid */}
        <div className="mt-12 grid gap-8 lg:grid-cols-3">
          {/* Left: plans + add-ons */}
          <div className="lg:col-span-2">
            {/* Plans */}
            <fieldset>
              <legend className="mb-4 text-sm font-semibold uppercase tracking-[0.12em] text-slate-500 dark:text-slate-400">
                1 · Choose a plan
              </legend>
              <div className="grid gap-4 sm:grid-cols-3">
                {PLANS.map((p) => {
                  const selected = p.id === planId;
                  const shown =
                    billing === "annual"
                      ? p.monthly * ANNUAL_FACTOR
                      : p.monthly;
                  return (
                    <label
                      key={p.id}
                      className={`group relative flex cursor-pointer flex-col rounded-2xl border p-5 transition-all duration-200 ${
                        selected
                          ? "border-indigo-500 bg-white shadow-xl shadow-indigo-500/10 ring-2 ring-indigo-500/40 dark:bg-slate-900"
                          : "border-slate-200 bg-white/60 hover:border-slate-300 hover:bg-white dark:border-white/10 dark:bg-white/[0.03] dark:hover:border-white/20"
                      }`}
                    >
                      <input
                        type="radio"
                        name={`${groupName}-plan`}
                        checked={selected}
                        onChange={() => setPlanId(p.id)}
                        className="peer sr-only"
                      />
                      <span className="pointer-events-none absolute inset-0 rounded-2xl opacity-0 ring-2 ring-indigo-500 ring-offset-2 ring-offset-white transition peer-focus-visible:opacity-100 dark:ring-offset-slate-950" />

                      <div className="flex items-center justify-between">
                        <span className="text-base font-semibold">{p.name}</span>
                        <span
                          className={`flex h-5 w-5 items-center justify-center rounded-full border transition ${
                            selected
                              ? "border-indigo-500 bg-indigo-500 text-white"
                              : "border-slate-300 text-transparent dark:border-slate-600"
                          }`}
                        >
                          <CheckIcon className="h-3.5 w-3.5" />
                        </span>
                      </div>

                      {p.popular && (
                        <span
                          className="pxaddon-anim mt-2 inline-flex w-fit items-center rounded-full bg-[length:200%_100%] bg-gradient-to-r from-amber-400 via-amber-200 to-amber-400 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-amber-900"
                          style={{ animation: "pxaddon-sheen 3.5s linear infinite" }}
                        >
                          Most popular
                        </span>
                      )}

                      <div className="mt-3 flex items-baseline gap-1">
                        <span className="text-2xl font-bold tracking-tight">
                          {money(shown)}
                        </span>
                        <span className="text-xs font-medium text-slate-500 dark:text-slate-400">
                          /mo
                        </span>
                      </div>

                      <p className="mt-2 text-xs leading-relaxed text-slate-600 dark:text-slate-400">
                        {p.tagline}
                      </p>

                      <ul className="mt-4 space-y-2 border-t border-slate-100 pt-4 dark:border-white/5">
                        {p.features.map((f) => (
                          <li
                            key={f}
                            className="flex items-start gap-2 text-xs text-slate-700 dark:text-slate-300"
                          >
                            <CheckIcon className="mt-0.5 h-3.5 w-3.5 shrink-0 text-indigo-500 dark:text-indigo-400" />
                            <span>{f}</span>
                          </li>
                        ))}
                      </ul>
                    </label>
                  );
                })}
              </div>
            </fieldset>

            {/* Add-ons */}
            <fieldset className="mt-10">
              <legend className="mb-4 text-sm font-semibold uppercase tracking-[0.12em] text-slate-500 dark:text-slate-400">
                2 · Select add-ons
              </legend>
              <div className="grid gap-3 sm:grid-cols-2">
                {ADDONS.map((a) => {
                  const checked = addOns.includes(a.id);
                  const shown =
                    billing === "annual" ? a.monthly * ANNUAL_FACTOR : a.monthly;
                  return (
                    <label
                      key={a.id}
                      className={`group relative flex cursor-pointer items-start gap-3 rounded-xl border p-4 transition-all duration-200 ${
                        checked
                          ? "border-violet-500 bg-violet-50/60 dark:border-violet-400/50 dark:bg-violet-500/10"
                          : "border-slate-200 bg-white/60 hover:border-slate-300 hover:bg-white dark:border-white/10 dark:bg-white/[0.03] dark:hover:border-white/20"
                      }`}
                    >
                      <input
                        type="checkbox"
                        checked={checked}
                        onChange={() => toggleAddOn(a.id)}
                        className="peer sr-only"
                      />
                      <span className="pointer-events-none absolute inset-0 rounded-xl opacity-0 ring-2 ring-violet-500 ring-offset-2 ring-offset-white transition peer-focus-visible:opacity-100 dark:ring-offset-slate-950" />

                      <span
                        className={`mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md border transition ${
                          checked
                            ? "border-violet-500 bg-violet-500 text-white"
                            : "border-slate-300 text-transparent dark:border-slate-600"
                        }`}
                      >
                        {checked && (
                          <CheckIcon
                            className="pxaddon-anim h-3.5 w-3.5"
                            style={{ animation: "pxaddon-pop 0.28s ease-out" }}
                          />
                        )}
                      </span>

                      <span className="min-w-0 flex-1">
                        <span className="flex items-center justify-between gap-2">
                          <span className="text-sm font-semibold">{a.name}</span>
                          <span className="whitespace-nowrap text-sm font-semibold text-violet-600 dark:text-violet-300">
                            +{money(shown)}
                            <span className="text-[11px] font-medium text-slate-400">
                              /mo
                            </span>
                          </span>
                        </span>
                        <span className="mt-1 block text-xs leading-relaxed text-slate-600 dark:text-slate-400">
                          {a.desc}
                        </span>
                      </span>
                    </label>
                  );
                })}
              </div>
            </fieldset>
          </div>

          {/* Right: summary */}
          <motion.aside
            {...fade}
            className="lg:col-span-1"
            aria-label="Order summary"
          >
            <div className="lg:sticky lg:top-8">
              <div className="overflow-hidden rounded-3xl border border-slate-200 bg-white/90 shadow-2xl shadow-slate-900/5 backdrop-blur dark:border-white/10 dark:bg-slate-900/80 dark:shadow-black/40">
                <div className="relative bg-gradient-to-br from-indigo-600 via-violet-600 to-indigo-700 px-6 py-7 text-white">
                  <div
                    aria-hidden="true"
                    className="absolute inset-0 opacity-30"
                    style={{
                      backgroundImage:
                        "radial-gradient(circle at 20% 20%, rgba(255,255,255,0.4) 0, transparent 45%)",
                    }}
                  />
                  <div className="relative">
                    <p className="text-xs font-semibold uppercase tracking-[0.14em] text-indigo-100">
                      Your total
                    </p>
                    <div className="mt-2 flex items-end gap-1.5">
                      <motion.span className="text-5xl font-bold tracking-tight">
                        {priceText}
                      </motion.span>
                      <span className="mb-1.5 text-sm font-medium text-indigo-100">
                        /mo
                      </span>
                    </div>
                    <p className="mt-2 text-sm text-indigo-100">
                      {billing === "annual"
                        ? `${money(billedYearly)} billed yearly`
                        : "Billed monthly · cancel anytime"}
                    </p>
                  </div>
                </div>

                <div className="px-6 py-5">
                  <dl className="space-y-2.5 text-sm">
                    <div className="flex items-center justify-between">
                      <dt className="text-slate-600 dark:text-slate-400">
                        {plan.name} plan
                      </dt>
                      <dd className="font-medium tabular-nums">
                        {money(plan.monthly)}/mo
                      </dd>
                    </div>

                    {ADDONS.filter((a) => addOns.includes(a.id)).map((a) => (
                      <div
                        key={a.id}
                        className="flex items-center justify-between"
                      >
                        <dt className="flex items-center gap-2 text-slate-600 dark:text-slate-400">
                          <span className="h-1 w-1 rounded-full bg-violet-400" />
                          {a.name}
                        </dt>
                        <dd className="font-medium tabular-nums text-violet-600 dark:text-violet-300">
                          +{money(a.monthly)}/mo
                        </dd>
                      </div>
                    ))}

                    {addOns.length === 0 && (
                      <p className="text-xs italic text-slate-400">
                        No add-ons selected yet.
                      </p>
                    )}

                    <div className="!mt-4 border-t border-slate-100 pt-3 dark:border-white/10">
                      <div className="flex items-center justify-between">
                        <dt className="text-slate-600 dark:text-slate-400">
                          Subtotal
                        </dt>
                        <dd className="font-medium tabular-nums">
                          {money(subtotal)}/mo
                        </dd>
                      </div>
                      {billing === "annual" && (
                        <div className="mt-2 flex items-center justify-between">
                          <dt className="flex items-center gap-1.5 text-emerald-600 dark:text-emerald-400">
                            <CheckIcon className="h-3.5 w-3.5" />
                            Annual discount
                          </dt>
                          <dd className="font-semibold tabular-nums text-emerald-600 dark:text-emerald-400">
                            −{money(yearlySavings / 12)}/mo
                          </dd>
                        </div>
                      )}
                    </div>
                  </dl>

                  {billing === "annual" && (
                    <p className="mt-4 rounded-xl bg-emerald-50 px-3 py-2 text-center text-xs font-medium text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-300">
                      You save {money(yearlySavings)} per year
                    </p>
                  )}

                  <button
                    type="button"
                    className="group mt-5 flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 px-5 py-3.5 text-sm font-semibold text-white shadow-lg shadow-indigo-500/25 transition hover:shadow-xl hover:shadow-indigo-500/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white active:scale-[0.99] dark:focus-visible:ring-offset-slate-900"
                  >
                    Continue to checkout
                    <ArrowIcon className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
                  </button>

                  <p className="mt-3 text-center text-xs text-slate-500 dark:text-slate-400">
                    14-day free trial · No card required
                  </p>
                </div>
              </div>
            </div>
          </motion.aside>
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

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 quote

Similar components

Browse all →
Simple Pricing

Simple Pricing

Original

A three-tier pricing section with a highlighted popular plan.

Single Plan Pricing Card

Single Plan Pricing Card

Original

A focused one-plan pricing card with price, trial badge, feature list, call to action and a money-back trust note, ideal when you sell a single flat-rate product.

Three Tier Pricing With Monthly Annual Toggle

Three Tier Pricing With Monthly Annual Toggle

Original

A three-tier pricing grid with a pure-CSS monthly and annual billing toggle that swaps every price with no JavaScript, using a native checkbox and Tailwind group-has state.

Plan Feature Comparison Table

Plan Feature Comparison Table

Original

A responsive, semantic feature comparison table across three plans with grouped rows, tick and dash indicators and screen-reader labels so visitors can weigh every feature side by side.

Staggered Glow Pricing Tiers

Staggered Glow Pricing Tiers

Original

A three-tier pricing section whose cards reveal in a staggered spring cascade, with a rotating conic-gradient glow on the popular plan and prices that count up smoothly when you toggle monthly and annual billing.

Spotlight Cursor Pricing Cards

Spotlight Cursor Pricing Cards

Original

Pricing cards that light up with a cursor-following radial spotlight, drift over floating background orbs and rise into view with a staggered spring entrance, plus a pulsing glow on the popular plan.

Highlight Follow Pricing Plans

Highlight Follow Pricing Plans

Original

A pricing row where an animated gradient highlight glides via shared layout animation to whichever plan you hover, a three-way billing switch slides its pill, prices recount instantly and a perks marquee scrolls beneath.

Three Tier Pricing

Three Tier Pricing

Original

three-tier pricing cards

Toggle Annual Pricing

Toggle Annual Pricing

Original

pricing with monthly/annual toggle

Single Highlight Pricing

Single Highlight Pricing

Original

single highlighted plan

Comparison Table Pricing

Comparison Table Pricing

Original

pricing comparison table

Cards Gradient Pricing

Cards Gradient Pricing

Original

gradient pricing cards