Web InnoventixFreeCode

Comparison Table Pricing

Original · free

pricing comparison table

byWeb InnoventixReact + Tailwind
pricexcomparisontablepricing
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-comparison-table.json
pricex-comparison-table.tsx
"use client";

import { Fragment, useId, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Billing = "monthly" | "annual";
type FeatureValue = boolean | string;

interface Plan {
  id: string;
  name: string;
  tagline: string;
  monthly: number;
  annual: number;
  cta: string;
  popular?: boolean;
}

interface Feature {
  label: string;
  values: [FeatureValue, FeatureValue, FeatureValue];
}

interface FeatureGroup {
  name: string;
  features: Feature[];
}

const PLANS: Plan[] = [
  {
    id: "starter",
    name: "Starter",
    tagline: "For solo builders finding their footing.",
    monthly: 12,
    annual: 9,
    cta: "Start free trial",
  },
  {
    id: "growth",
    name: "Growth",
    tagline: "For teams shipping every single week.",
    monthly: 29,
    annual: 23,
    cta: "Start free trial",
    popular: true,
  },
  {
    id: "scale",
    name: "Scale",
    tagline: "For orgs that need real control.",
    monthly: 79,
    annual: 63,
    cta: "Talk to sales",
  },
];

const GROUPS: FeatureGroup[] = [
  {
    name: "Core workspace",
    features: [
      { label: "Active projects", values: ["3", "Unlimited", "Unlimited"] },
      { label: "Team members", values: ["2", "15", "Unlimited"] },
      { label: "File storage", values: ["10 GB", "250 GB", "2 TB"] },
      { label: "Version history", values: ["7 days", "90 days", "Unlimited"] },
    ],
  },
  {
    name: "Collaboration",
    features: [
      { label: "Real-time editing", values: [true, true, true] },
      { label: "Comments & @mentions", values: [true, true, true] },
      { label: "Guest & client access", values: [false, true, true] },
      { label: "Roles & permissions", values: [false, true, true] },
    ],
  },
  {
    name: "Automation & AI",
    features: [
      { label: "Automation runs / mo", values: ["500", "10,000", "Unlimited"] },
      { label: "AI drafting assist", values: [false, true, true] },
      { label: "Custom workflow builder", values: [false, false, true] },
    ],
  },
  {
    name: "Security & support",
    features: [
      { label: "SSO / SAML sign-in", values: [false, false, true] },
      { label: "Audit logs", values: [false, false, true] },
      { label: "Priority support", values: [false, true, true] },
      { label: "99.9% uptime SLA", values: [false, false, true] },
    ],
  },
];

const POPULAR_INDEX = PLANS.findIndex((p) => p.popular);

function CheckIcon() {
  return (
    <svg
      viewBox="0 0 20 20"
      fill="none"
      aria-hidden="true"
      className="h-3.5 w-3.5"
    >
      <path
        d="M4.5 10.5l3.2 3.2 7.8-8.4"
        stroke="currentColor"
        strokeWidth="2.2"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function DashIcon() {
  return (
    <svg
      viewBox="0 0 20 20"
      fill="none"
      aria-hidden="true"
      className="h-3.5 w-3.5"
    >
      <path
        d="M5 10h10"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
      />
    </svg>
  );
}

function SparkIcon() {
  return (
    <svg
      viewBox="0 0 20 20"
      fill="currentColor"
      aria-hidden="true"
      className="h-3.5 w-3.5"
    >
      <path d="M10 1.5l1.7 4.6 4.8 1.7-4.8 1.7L10 14.1 8.3 9.5 3.5 7.8l4.8-1.7L10 1.5z" />
    </svg>
  );
}

function ShieldIcon() {
  return (
    <svg
      viewBox="0 0 20 20"
      fill="none"
      aria-hidden="true"
      className="h-4 w-4"
    >
      <path
        d="M10 2.2l6 2.2v4.2c0 3.7-2.5 6.7-6 8-3.5-1.3-6-4.3-6-8V4.4l6-2.2z"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinejoin="round"
      />
      <path
        d="M7.4 10.1l1.9 1.9 3.5-3.9"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function ValueCell({ value }: { value: FeatureValue }) {
  if (value === true) {
    return (
      <span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400">
        <CheckIcon />
        <span className="sr-only">Included</span>
      </span>
    );
  }
  if (value === false) {
    return (
      <span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-slate-100 text-slate-400 dark:bg-zinc-800 dark:text-zinc-600">
        <DashIcon />
        <span className="sr-only">Not included</span>
      </span>
    );
  }
  return (
    <span className="text-sm font-medium text-slate-700 dark:text-zinc-200">
      {value}
    </span>
  );
}

export default function PricexComparisonTable() {
  const reduce = useReducedMotion();
  const [billing, setBilling] = useState<Billing>("annual");
  const headingId = useId();

  const colClass = (i: number) =>
    i === POPULAR_INDEX
      ? "bg-indigo-50/70 dark:bg-indigo-500/10"
      : "";

  const priceAnim = reduce
    ? {}
    : {
        initial: { opacity: 0, y: 8 },
        animate: { opacity: 1, y: 0 },
        exit: { opacity: 0, y: -8 },
        transition: { duration: 0.22, ease: "easeOut" as const },
      };

  return (
    <section
      aria-labelledby={headingId}
      className="relative w-full overflow-hidden bg-white px-4 py-20 text-slate-900 sm:px-6 sm:py-28 dark:bg-zinc-950 dark:text-zinc-50"
    >
      <style>{`
        @keyframes pricex-fade-up {
          from { opacity: 0; transform: translateY(16px); }
          to { opacity: 1; transform: translateY(0); }
        }
        @keyframes pricex-shimmer {
          0% { background-position: -140% 0; }
          100% { background-position: 240% 0; }
        }
        @keyframes pricex-glow {
          0%, 100% { opacity: 0.35; }
          50% { opacity: 0.7; }
        }
        .pricex-badge-shimmer {
          background-image: linear-gradient(
            110deg,
            transparent 20%,
            rgba(255,255,255,0.55) 50%,
            transparent 80%
          );
          background-size: 200% 100%;
          animation: pricex-shimmer 3.4s linear infinite;
        }
        .pricex-orb { animation: pricex-glow 6s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .pricex-badge-shimmer,
          .pricex-orb { animation: none !important; }
        }
      `}</style>

      {/* ambient background */}
      <div
        aria-hidden="true"
        className="pricex-orb pointer-events-none absolute -top-24 left-1/2 h-72 w-72 -translate-x-1/2 rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-500/20"
      />
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-x-0 bottom-0 h-px bg-gradient-to-r from-transparent via-slate-200 to-transparent dark:via-zinc-800"
      />

      <div className="relative mx-auto max-w-6xl">
        {/* header */}
        <div className="mx-auto max-w-2xl text-center">
          <span className="inline-flex items-center gap-1.5 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-indigo-700 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
            <SparkIcon />
            Pricing
          </span>
          <h2
            id={headingId}
            className="mt-5 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl dark:text-white"
          >
            Pricing that scales with your team
          </h2>
          <p className="mt-4 text-base leading-relaxed text-slate-600 dark:text-zinc-400">
            Compare every plan side by side. Switch tiers or cancel anytime —
            no hidden fees, no surprise overage bills, no forced annual lock-in.
          </p>
        </div>

        {/* billing toggle */}
        <div className="mt-8 flex items-center justify-center">
          <div
            role="group"
            aria-label="Billing period"
            className="inline-flex items-center rounded-full border border-slate-200 bg-slate-100 p-1 dark:border-zinc-800 dark:bg-zinc-900"
          >
            {(["monthly", "annual"] as const).map((mode) => {
              const active = billing === mode;
              return (
                <button
                  key={mode}
                  type="button"
                  aria-pressed={active}
                  onClick={() => setBilling(mode)}
                  className="relative rounded-full px-4 py-1.5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 dark:focus-visible:ring-offset-zinc-900"
                >
                  {active ? (
                    <motion.span
                      layoutId={reduce ? undefined : "pricex-billing"}
                      className="absolute inset-0 rounded-full bg-white shadow-sm dark:bg-zinc-700"
                      transition={{ type: "spring", stiffness: 420, damping: 34 }}
                    />
                  ) : null}
                  <span
                    className={
                      "relative z-10 flex items-center gap-1.5 " +
                      (active
                        ? "text-slate-900 dark:text-white"
                        : "text-slate-500 dark:text-zinc-400")
                    }
                  >
                    {mode === "monthly" ? "Monthly" : "Annual"}
                    {mode === "annual" ? (
                      <span className="rounded-full bg-emerald-100 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400">
                        Save 20%
                      </span>
                    ) : null}
                  </span>
                </button>
              );
            })}
          </div>
        </div>

        {/* table */}
        <motion.div
          initial={reduce ? false : { opacity: 0, y: 24 }}
          whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
          viewport={{ once: true, amount: 0.12 }}
          transition={{ duration: 0.5, ease: "easeOut" }}
          className="mt-12 overflow-x-auto rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-950"
        >
          <table className="w-full min-w-[760px] border-collapse text-left">
            <caption className="sr-only">
              Feature comparison across Starter, Growth and Scale plans
            </caption>
            <thead>
              <tr>
                <th
                  scope="col"
                  className="sticky left-0 z-30 w-[30%] bg-white p-5 align-bottom dark:bg-zinc-950"
                >
                  <span className="text-sm font-semibold text-slate-500 dark:text-zinc-400">
                    Compare features
                  </span>
                </th>
                {PLANS.map((plan, i) => {
                  const price = billing === "annual" ? plan.annual : plan.monthly;
                  return (
                    <th
                      key={plan.id}
                      scope="col"
                      className={
                        "relative border-l border-slate-100 p-5 align-bottom dark:border-zinc-800/80 " +
                        colClass(i)
                      }
                    >
                      {plan.popular ? (
                        <span className="absolute -top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-full bg-indigo-600 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wide text-white shadow-sm">
                          <span className="pricex-badge-shimmer absolute inset-0" />
                          <span className="relative flex items-center gap-1">
                            <SparkIcon />
                            Most popular
                          </span>
                        </span>
                      ) : null}
                      <div className="text-base font-bold text-slate-900 dark:text-white">
                        {plan.name}
                      </div>
                      <p className="mt-1 min-h-[2.5rem] text-xs leading-snug text-slate-500 dark:text-zinc-400">
                        {plan.tagline}
                      </p>
                      <div className="mt-3 flex items-baseline gap-1">
                        <span className="text-sm font-medium text-slate-500 dark:text-zinc-400">
                          $
                        </span>
                        <AnimatePresence mode="wait" initial={false}>
                          <motion.span
                            key={`${plan.id}-${billing}`}
                            {...priceAnim}
                            className="text-3xl font-bold tabular-nums text-slate-900 dark:text-white"
                          >
                            {price}
                          </motion.span>
                        </AnimatePresence>
                        <span className="text-sm font-medium text-slate-500 dark:text-zinc-400">
                          /mo
                        </span>
                      </div>
                      <p className="mt-1 text-xs text-slate-400 dark:text-zinc-500">
                        {billing === "annual"
                          ? `Billed $${plan.annual * 12}/yr`
                          : "Billed monthly"}
                      </p>
                      <a
                        href="#get-started"
                        className={
                          "mt-4 flex w-full items-center justify-center rounded-lg px-4 py-2 text-sm font-semibold transition-colors focus-visible: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-zinc-950 " +
                          (plan.popular
                            ? "bg-indigo-600 text-white hover:bg-indigo-500"
                            : "border border-slate-300 text-slate-800 hover:border-slate-400 hover:bg-slate-50 dark:border-zinc-700 dark:text-zinc-100 dark:hover:border-zinc-600 dark:hover:bg-zinc-900")
                        }
                      >
                        {plan.cta}
                      </a>
                    </th>
                  );
                })}
              </tr>
            </thead>

            <tbody>
              {GROUPS.map((group) => (
                <Fragment key={group.name}>
                  <tr>
                    <th
                      scope="colgroup"
                      colSpan={4}
                      className="sticky left-0 z-20 bg-slate-50 px-5 py-2.5 text-xs font-bold uppercase tracking-wide text-slate-500 dark:bg-zinc-900 dark:text-zinc-400"
                    >
                      {group.name}
                    </th>
                  </tr>
                  {group.features.map((feature) => (
                    <tr
                      key={`${group.name}-${feature.label}`}
                      className="group border-t border-slate-100 dark:border-zinc-800/70"
                    >
                      <th
                        scope="row"
                        className="sticky left-0 z-10 bg-white p-5 text-sm font-medium text-slate-700 group-hover:bg-slate-50 dark:bg-zinc-950 dark:text-zinc-200 dark:group-hover:bg-zinc-900/60"
                      >
                        {feature.label}
                      </th>
                      {feature.values.map((value, i) => (
                        <td
                          key={i}
                          className={
                            "border-l border-slate-100 p-5 text-center dark:border-zinc-800/70 " +
                            colClass(i)
                          }
                        >
                          <ValueCell value={value} />
                        </td>
                      ))}
                    </tr>
                  ))}
                </Fragment>
              ))}
            </tbody>
          </table>
        </motion.div>

        {/* footer note */}
        <div className="mt-6 flex flex-col items-center justify-center gap-2 text-sm text-slate-500 sm:flex-row sm:gap-6 dark:text-zinc-400">
          <span className="inline-flex items-center gap-1.5">
            <span className="text-emerald-600 dark:text-emerald-400">
              <ShieldIcon />
            </span>
            30-day money-back guarantee
          </span>
          <span className="inline-flex items-center gap-1.5">
            <span className="text-indigo-600 dark:text-indigo-400">
              <CheckIcon />
            </span>
            No credit card required to trial
          </span>
          <span className="hidden text-slate-300 sm:inline dark:text-zinc-700">
            •
          </span>
          <span>All prices in USD</span>
        </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

Cards Gradient Pricing

Cards Gradient Pricing

Original

gradient pricing cards

Minimal Pricing

Minimal Pricing

Original

minimal pricing tiers