Section

Pricing

Plans side by side in windows, with the figure, compared rows and a full-width key; one plan featured.

Text referenceSource and examples as JSON

npx -y github:anwarlemu/opencommerce-brand-cli#release add pricing --surface site

Run from the target React 19.2+ app root with Tailwind CSS 4 and the @/* source alias.

Open alone
pricing.preview
Preview

Usage

use-it-for.md

Pass title, description, the compared rows and two plans with one value per row. Feature at most one plan.

never.md
  • More than one featured plan
  • More than three plans
  • Strikethrough prices without a reason
  • Check lists in place of compared rows
  • A blue window

Exports: Pricing, Plan, PlanAction. Read the examples for composition and props.

Pricing props: title, description, rows (the labels compared), plans ({ name, badge, price, unit, blurb, values, action ({ label, href } or a node), featured, file }[]), plus div props.

Dependencies: section, button, window.

Example

examples/pricing.tsx
import { Pricing } from "@/components/site/pricing";

export function Site() {
  return (
    <section className="py-20">
      <Pricing
        title="Pricing"
        description="Start free while you build. Move to Standard when your agent is buying for real."
        rows={["Platform fees", "Merchants", "PSPs"]}
        plans={[
          { name: "Free", price: "$0", unit: "while you build", blurb: "Everything you need to wire up your agent and make its first purchases.", values: ["N/A", "5 merchants", "2 PSPs"], action: { label: "Try free", href: "https://app.agentcard.sh" } },
          { name: "Standard", badge: "Every merchant", price: "1%", unit: "of each completed order", blurb: "For agents buying for real, across every store and payment provider.", values: ["1%", "Every merchant", "Every PSP"], featured: true, action: { label: "Get started", href: "https://app.agentcard.sh" } },
        ]}
      />
    </section>
  );
}

Source

components/site/pricing.tsx
import * as React from "react";

import { SectionHeading } from "@/components/site/section";
import { Button } from "@/components/ui/button";
import { Window } from "@/components/ui/window";
import { cn } from "@/lib/utils";

// Pricing (the opencommerce site, pricing.tsx): the centered heading, then the plans side by side in the 800px column,
// each in a window named after it (free.plan). A plan has its name at 20px, the figure at 44px in the display face with
// its unit in 14px muted type beside it, a line on who it is for, then the compared rows on hairlines (the label muted
// at the left, the value at the right) and a full-width key. The featured plan carries a small mono tag in the pale blue,
// sets its values in blue and takes the blue key; the others take the white key. Rows line up across plans because
// every plan answers the same `rows`.

type PlanAction = { label: React.ReactNode; href: string };

type Plan = {
  name: string;
  /** A small tag beside the name (the featured plan's). */
  badge?: React.ReactNode;
  price: React.ReactNode;
  /** Beside the figure: "of each completed order", "/ month". */
  unit?: React.ReactNode;
  /** Who it is for, under the figure. */
  blurb?: React.ReactNode;
  /** One value per row, in the order of `rows`. */
  values: React.ReactNode[];
  /** The plan's key: a label and href, or your own element. */
  action?: PlanAction | React.ReactNode;
  /** The plan to steer toward: blue values and the blue key. One at most. */
  featured?: boolean;
  /** The window's name; defaults to the plan's slug with .plan. */
  file?: string;
};

const slug = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
const isAction = (a: Plan["action"]): a is PlanAction => typeof a === "object" && a !== null && !React.isValidElement(a) && "href" in a;

function PlanWindow({ plan, rows }: { plan: Plan; rows: React.ReactNode[] }) {
  return (
    <Window name={plan.file ?? `${slug(plan.name)}.plan`} bodyClassName="flex flex-col">
      <div className="flex flex-1 flex-col p-6">
        <div className="flex items-center justify-between gap-3">
          <h3 className="font-display text-h4 leading-snug font-medium tracking-tight">{plan.name}</h3>
          {plan.badge && <span className="rounded-chip bg-tint px-2 py-1 font-mono text-caption leading-none text-fg-brand">{plan.badge}</span>}
        </div>
        <p className="mt-5 flex items-baseline gap-2">
          <span className="font-display text-h2 leading-none font-medium tracking-hero">{plan.price}</span>
          {plan.unit && <span className="text-body-sm text-fg-muted">{plan.unit}</span>}
        </p>
        {plan.blurb && <p className="mt-3 text-button leading-relaxed text-fg-muted">{plan.blurb}</p>}
        {rows.length > 0 && (
          <dl className="mt-6 border-t border-line">
            {rows.map((row, i) => (
              <div key={i} className="flex items-baseline justify-between gap-4 border-b border-line py-3.5 text-body-sm">
                <dt className="text-fg-muted">{row}</dt>
                <dd className={cn("text-right", plan.featured && "text-fg-brand")}>{plan.values[i]}</dd>
              </div>
            ))}
          </dl>
        )}
        <div className="mt-auto pt-7">
          {isAction(plan.action) ? (
            <Button asChild variant={plan.featured ? "primary" : "secondary"} className="w-full">
              <a href={plan.action.href}>{plan.action.label}</a>
            </Button>
          ) : (
            plan.action
          )}
        </div>
      </div>
    </Window>
  );
}

function Pricing({
  title,
  description,
  rows = [],
  plans,
  className,
  ...props
}: Omit<React.ComponentProps<"div">, "title"> & { title?: React.ReactNode; description?: React.ReactNode; rows?: React.ReactNode[]; plans: Plan[] }) {
  return (
    <div data-slot="pricing" className={cn("w-full", className)} {...props}>
      {title && <SectionHeading title={title} description={description} />}
      <div className={cn("mx-auto grid max-w-(--ds-layout-medium) gap-5 md:gap-6", plans.length > 1 && "md:grid-cols-2")}>
        {plans.map((plan) => (
          <PlanWindow key={plan.name} plan={plan} rows={rows} />
        ))}
      </div>
    </div>
  );
}

export { Pricing, type Plan, type PlanAction };

Reference: opencommerce-agentcard-web src/components/pricing.tsx, src/lib/content.ts (PRICING).