Pricing

7 blocks

Pricing 01

Preview
npx nateui@latest add PricingPlansToggle
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Segment from '@/components/ui/Segment';
import Tag from '@/components/ui/Tag';
import IconFrame from '@/components/composites/IconFrame';
import { PiCheckCircleFill, PiCrownFill, PiCubeFill, PiLightningFill } from 'react-icons/pi';

type BillingCycle = 'monthly' | 'yearly';

type Tier = {
  name: string;
  description: string;
  monthlyPrice: number;
  features: string[];
  popular?: boolean;
};

const tierIcon = {
  Basic: PiCubeFill,
  Pro: PiLightningFill,
  Premium: PiCrownFill,
} as const;

const tiers: Tier[] = [
  {
    name: 'Basic',
    description: 'For individuals just getting started.',
    monthlyPrice: 12,
    features: [
      '3 active projects',
      '10 GB storage',
      'Email support',
      'Standard exports',
      '1 team member',
    ],
  },
  {
    name: 'Pro',
    description: 'For growing teams that need more power.',
    monthlyPrice: 39,
    popular: true,
    features: [
      'Unlimited projects',
      '100 GB storage',
      'Priority support',
      'Advanced exports',
      'Up to 10 team members',
    ],
  },
  {
    name: 'Premium',
    description: 'For organizations that need it all.',
    monthlyPrice: 89,
    features: [
      'Unlimited projects',
      '1 TB storage',
      'Dedicated support',
      'Advanced exports and API access',
      'Unlimited team members',
    ],
  },
];

export default function PricingPlansToggle() {
  const [cycle, setCycle] = useState<BillingCycle>('monthly');
  const yearly = cycle === 'yearly';

  return (
    <section aria-labelledby="pricing-plans-toggle-title" className="w-full">
      <div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
        <div>
          <h3
            id="pricing-plans-toggle-title"
            className="text-2xl font-semibold"
          >
            Plans that grow with your team
          </h3>
          <p className="mt-1 max-w-md text-muted-foreground">
            Simple, transparent pricing that scales as your team grows.
          </p>
        </div>

        <Segment
          value={cycle}
          onChange={(value) => setCycle(value as BillingCycle)}
          aria-label="Billing cycle"
        >
          <Segment.Item value="monthly">Monthly</Segment.Item>
          <Segment.Item value="yearly">Yearly</Segment.Item>
        </Segment>
      </div>

      <div className="mt-8 grid grid-cols-1 gap-4 md:grid-cols-3">
        {tiers.map((tier) => {
          const price = yearly ? Math.round((tier.monthlyPrice * 10) / 12) : tier.monthlyPrice;
          const Icon = tierIcon[tier.name as keyof typeof tierIcon];

          return (
            <Card key={tier.name}>
              <div className="flex items-start justify-between gap-2">
                <IconFrame variant="layered">
                  <Icon
                    aria-hidden="true"
                    className={tier.popular ? 'text-lg text-primary' : 'text-lg'}
                  />
                </IconFrame>
                {tier.popular && (
                  <Tag className="border-0 bg-primary-soft text-primary">Popular</Tag>
                )}
              </div>

              <h5 className="mt-4 text-lg font-semibold text-card-foreground">{tier.name}</h5>
              <p className="text-muted-foreground">{tier.description}</p>

              <div className="mt-4 flex items-baseline gap-1">
                <span className="text-2xl font-semibold text-card-foreground tabular-nums">
                  ${price}
                </span>
                <span className="text-muted-foreground">/mo</span>
              </div>
              <p className="text-xs text-muted-foreground">
                {yearly ? 'Billed annually' : 'Billed monthly'}
              </p>

              <div className="mt-4 border-t pt-4">
                <div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                  Features
                </div>
                <ul className="mt-4 space-y-2">
                  {tier.features.map((feature) => (
                    <li key={feature} className="flex items-start gap-2">
                      <PiCheckCircleFill aria-hidden="true" className="mt-0.5 shrink-0 text-lg text-success" />
                      <span className="min-w-0">{feature}</span>
                    </li>
                  ))}
                </ul>
              </div>

              <Button type="button" block variant={tier.popular ? 'solid' : 'default'} className="mt-4">
                Get started
              </Button>
            </Card>
          );
        })}
      </div>
    </section>
  );
}

Pricing 02

Preview
npx nateui@latest add PricingPlanComparisonTable
Dark
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import classNames from '@/utils/classNames';
import { PiCheck } from 'react-icons/pi';

type Plan = {
  name: string;
  headline: string;
  description: string;
  cta: string;
  featured?: boolean;
  outlined?: boolean;
  inactive?: boolean;
};

const plans: Plan[] = [
  {
    name: 'Basic',
    headline: 'Free',
    description: 'The essentials for a growing team: track and manage your everyday work.',
    cta: 'Current plan',
    inactive: true,
  },
  {
    name: 'Pro',
    headline: '$24/mo',
    description: 'Best for a small team running active projects: move faster and stay in sync.',
    cta: 'Upgrade to Pro',
    featured: true,
  },
  {
    name: 'Custom',
    headline: 'Custom rate',
    description: 'Pick this plan if you need a custom solution for your business.',
    cta: 'Talk to us',
    outlined: true,
  },
];

type Cell =
  | { kind: 'check' }
  | { kind: 'note'; note: string }
  | { kind: 'text'; value: string }
  | { kind: 'empty' };

type FeatureRow = {
  label: string;
  basic: Cell;
  pro: Cell;
};

type FeatureGroup = {
  name: string;
  rows: FeatureRow[];
};

const check: Cell = { kind: 'check' };
const empty: Cell = { kind: 'empty' };
const quote: Cell = { kind: 'text', value: 'Contact us for a quote' };

const featureGroups: FeatureGroup[] = [
  {
    name: 'Core features',
    rows: [
      { label: 'Team dashboard', basic: check, pro: check },
      {
        label: 'Advanced analytics',
        basic: { kind: 'note', note: 'Verify your workspace to enable this' },
        pro: { kind: 'note', note: 'Verify your workspace to enable this' },
      },
      { label: 'Scheduled reports', basic: empty, pro: empty },
    ],
  },
  {
    name: 'Workspace',
    rows: [
      { label: 'Shared file storage', basic: check, pro: check },
      {
        label: 'Storage overage',
        basic: { kind: 'text', value: '$0.40 per GB' },
        pro: { kind: 'text', value: 'No charge' },
      },
      {
        label: 'Transfer fee (includes bandwidth and processing charges)',
        basic: { kind: 'text', value: '8% + $0.20' },
        pro: { kind: 'text', value: '6.5% + $0.20' },
      },
    ],
  },
  {
    name: 'Support',
    rows: [
      { label: 'Community support', basic: check, pro: check },
      { label: 'Priority support', basic: empty, pro: check },
    ],
  },
  {
    name: 'Automation',
    rows: [
      { label: 'Workflow builder', basic: quote, pro: quote },
      { label: 'Custom API access (REST, GraphQL)', basic: quote, pro: quote },
    ],
  },
];

function CellContent({ cell }: { cell: Cell }) {
  if (cell.kind === 'check') {
    return <PiCheck aria-hidden="true" className="text-lg text-primary" />;
  }

  if (cell.kind === 'note') {
    return (
      <div className="flex items-start gap-2">
        <PiCheck aria-hidden="true" className="mt-0.5 shrink-0 text-lg text-primary" />
        <span className="text-xs italic leading-snug">{cell.note}</span>
      </div>
    );
  }

  if (cell.kind === 'text') {
    return <span>{cell.value}</span>;
  }

  return null;
}

export default function PricingPlanComparisonTable() {
  const [firstGroup, ...restGroups] = featureGroups;

  return (
    <section aria-labelledby="pricing-plan-comparison-title" className="w-full">
      <h4 id="pricing-plan-comparison-title" className="text-lg font-semibold text-foreground">
        Choose a subscription plan
      </h4>
      <p className="mt-1 text-muted-foreground">
        Select a plan that fits how your team works today.
      </p>

      <div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-3">
        {plans.map((plan) => (
          <Card
            key={plan.name}
            bodyClass="flex h-full flex-col p-4 text-center"
            className={plan.featured ? 'border-primary bg-primary' : undefined}
          >
            <div className={plan.featured ? 'text-primary-foreground/80' : 'text-muted-foreground'}>
              {plan.name}
            </div>

            <div
              className={classNames(
                'mt-2 text-2xl font-bold',
                plan.featured ? 'text-primary-foreground' : 'text-card-foreground',
              )}
            >
              {plan.headline}
            </div>

            <p
              className={classNames(
                'mt-4 flex-1',
                plan.featured ? 'text-primary-foreground/80' : 'text-muted-foreground',
              )}
            >
              {plan.description}
            </p>

            <Button
              type="button"
              block
              disabled={plan.inactive}
              variant={plan.featured ? 'solid' : 'default'}
              className={classNames(
                'mt-8',
                plan.featured && 'bg-white text-primary hover:bg-white/90',
              )}
            >
              {plan.cta}
            </Button>
          </Card>
        ))}
      </div>

      <h5 className="mt-8 text-lg font-semibold text-foreground">Compare features</h5>

      <div className="mt-4 overflow-hidden rounded-card border bg-card">
        <div className="overflow-x-auto">
          <div
            role="table"
            aria-label="Plan feature comparison"
            className="grid min-w-2xl grid-cols-3"
          >
          <div role="row" className="contents">
            <div role="columnheader" className="flex items-end px-4 py-4">
              <span className="text-base font-semibold text-primary">{firstGroup.name}</span>
            </div>
            <div role="columnheader" className="bg-primary/5 px-4 py-4">
              <div className="text-muted-foreground">Basic</div>
              <div className="text-base font-semibold text-foreground">Free</div>
            </div>
            <div role="columnheader" className="px-4 py-4">
              <div className="text-muted-foreground">Pro</div>
              <div className="text-base font-semibold text-foreground">$24 USD/mo</div>
            </div>
          </div>

          {firstGroup.rows.map((row) => (
            <div key={row.label} role="row" className="contents">
              <div role="rowheader" className="border-t p-4 text-foreground">
                {row.label}
              </div>
              <div role="cell" className="border-t bg-primary/5 p-4">
                <CellContent cell={row.basic} />
              </div>
              <div role="cell" className="border-t p-4">
                <CellContent cell={row.pro} />
              </div>
            </div>
          ))}

          {restGroups.flatMap((group) => [
            <div key={group.name} role="row" className="contents">
              <div role="rowheader" className="border-t px-4 py-4 text-base font-semibold text-primary">
                {group.name}
              </div>
              <div role="cell" className="border-t bg-primary/5" />
              <div role="cell" className="border-t" />
            </div>,
            ...group.rows.map((row) => (
              <div key={row.label} role="row" className="contents">
                <div role="rowheader" className="border-t p-4 text-foreground">
                  {row.label}
                </div>
                <div role="cell" className="border-t bg-primary/5 p-4">
                  <CellContent cell={row.basic} />
                </div>
                <div role="cell" className="border-t p-4">
                  <CellContent cell={row.pro} />
                </div>
              </div>
            )),
          ])}
          </div>
        </div>
      </div>
    </section>
  );
}

Pricing 03

Preview
npx nateui@latest add PricingPlanMatrixTable
Dark
import { Fragment } from 'react';
import Button from '@/components/ui/Button';
import Slider from '@/components/ui/Slider';
import Tag from '@/components/ui/Tag';
import Tooltip from '@/components/ui/Tooltip';
import Container from '@/components/composites/Container';
import classNames from '@/utils/classNames';
import { PiCheck, PiInfo, PiMinus } from 'react-icons/pi';

const seatMarks = [
  { value: 1 , label: '1 User' },
  { value: 50 },
  { value: 100 },
  { value: 150 },
  { value: 200 },
  { value: 250 },
  { value: 300, label: '300 Users' },
];

type Plan = {
  name: string;
  popular?: boolean;
  price: string;
  description: string;
};

const plans: Plan[] = [
  {
    name: 'Starter',
    popular: true,
    price: '$19',
    description: 'For small teams that need the essentials, nothing more.',
  },
  {
    name: 'Team',
    price: '$49',
    description: 'For growing teams that need automation and deeper reporting.',
  },
  {
    name: 'Enterprise',
    price: '$99',
    description: 'For organizations that need enterprise grade control.',
  },
];

type Cell =
  | { kind: 'check' }
  | { kind: 'dash' }
  | { kind: 'text'; value: string };

type FeatureRow = {
  label: string;
  tooltip: string;
  starter: Cell;
  team: Cell;
  enterprise: Cell;
};

type FeatureGroup = {
  name: string;
  rows: FeatureRow[];
};

const check: Cell = { kind: 'check' };
const dash: Cell = { kind: 'dash' };

const featureGroups: FeatureGroup[] = [
  {
    name: 'Core plan',
    rows: [
      {
        label: 'Team workspace',
        tooltip: 'A shared workspace for your whole team.',
        starter: check,
        team: check,
        enterprise: check,
      },
      {
        label: 'Seats',
        tooltip: 'Team members who can sign in and collaborate.',
        starter: { kind: 'text', value: '5' },
        team: { kind: 'text', value: '20' },
        enterprise: { kind: 'text', value: 'Unlimited' },
      },
      {
        label: 'Storage',
        tooltip: 'Combined file storage across your workspace.',
        starter: { kind: 'text', value: '20 GB' },
        team: { kind: 'text', value: '100 GB' },
        enterprise: { kind: 'text', value: 'Unlimited' },
      },
      {
        label: 'Standard support',
        tooltip: 'Email support with same-day response.',
        starter: check,
        team: check,
        enterprise: check,
      },
      {
        label: 'Workflow automation',
        tooltip: 'Trigger actions automatically from workspace events.',
        starter: dash,
        team: check,
        enterprise: check,
      },
      {
        label: 'Connected apps',
        tooltip: 'Link third-party tools to your workspace.',
        starter: dash,
        team: check,
        enterprise: check,
      },
    ],
  },
  {
    name: 'Insights',
    rows: [
      {
        label: 'Reporting',
        tooltip: 'Depth of the metrics available on your dashboard.',
        starter: { kind: 'text', value: 'Basic' },
        team: { kind: 'text', value: 'Advanced' },
        enterprise: { kind: 'text', value: 'Advanced' },
      },
      {
        label: 'Export data',
        tooltip: 'Download records as CSV or PDF.',
        starter: check,
        team: check,
        enterprise: check,
      },
      {
        label: 'Scheduled digests',
        tooltip: 'Automatic report delivery by email.',
        starter: check,
        team: check,
        enterprise: check,
      },
      {
        label: 'API access',
        tooltip: 'Read and write workspace data programmatically.',
        starter: dash,
        team: check,
        enterprise: check,
      },
      {
        label: 'Custom dashboards',
        tooltip: 'Build your own saved report views.',
        starter: dash,
        team: check,
        enterprise: check,
      },
      {
        label: 'Custom fields',
        tooltip: "Add fields specific to how your team works.",
        starter: dash,
        team: dash,
        enterprise: check,
      },
    ],
  },
  {
    name: 'Security & access',
    rows: [
      {
        label: 'Single sign-on',
        tooltip: 'Sign in with your existing identity provider.',
        starter: check,
        team: check,
        enterprise: check,
      },
      {
        label: 'Role-based permissions',
        tooltip: 'Restrict access by role across the workspace.',
        starter: dash,
        team: check,
        enterprise: check,
      },
      {
        label: 'Audit log',
        tooltip: 'A record of every change made in your workspace.',
        starter: dash,
        team: dash,
        enterprise: check,
      },
      {
        label: 'Data retention history',
        tooltip: 'Recover past versions of your workspace data.',
        starter: dash,
        team: dash,
        enterprise: check,
      },
    ],
  },
];

function CellContent({ cell }: { cell: Cell }) {
  if (cell.kind === 'check') {
    return (
      <>
        <PiCheck aria-hidden="true" className="text-lg text-success" />
        <span className="sr-only">Included</span>
      </>
    );
  }

  if (cell.kind === 'dash') {
    return (
      <>
        <PiMinus aria-hidden="true" className="text-lg" />
        <span className="sr-only">Not included</span>
      </>
    );
  }

  return <span>{cell.value}</span>;
}

export default function PricingPlanMatrixTable() {
  return (
    <section aria-labelledby="pricing-plan-matrix-title" className="w-full">
      <Container>
        <div>
          <h3 id="pricing-plan-matrix-title" className="text-2xl font-semibold text-foreground">
            Plans for every team size
          </h3>
          <p className="mt-1 text-muted-foreground">
            Straightforward pricing with every feature included, no matter your team size.
          </p>

          <div className="mt-8 mb-4 px-8">
            <Slider
              defaultValue={100}
              min={0}
              max={300}
              step={50}
              marks={seatMarks}
              showTooltipOnHover
              tooltip={(value) => `${value} Users`}
              thumbAriaLabel="Number of users"
            />
          </div>
        </div>

        <div className="mt-8 overflow-x-auto">
          <table className="w-full min-w-3xl table-fixed border-collapse">
            <colgroup>
              <col className="w-1/4" />
              <col className="w-1/4" />
              <col className="w-1/4" />
              <col className="w-1/4" />
            </colgroup>

            <thead>
              <tr className="border-b">
                <th scope="col">
                  <span className="sr-only">Feature</span>
                </th>
                {plans.map((plan) => (
                  <th key={plan.name} scope="col" className="px-4 py-4 text-left align-bottom">
                    <span className="inline-flex items-center gap-2 text-lg font-semibold text-foreground">
                      {plan.name}
                      {plan.popular && (
                        <Tag className="border-0 bg-primary-soft text-xs text-primary">Popular</Tag>
                      )}
                    </span>
                  </th>
                ))}
              </tr>
            </thead>

            <tbody>
              <tr>
                <th scope="row">
                  <span className="sr-only">Choose your plan</span>
                </th>
                {plans.map((plan) => (
                  <td key={plan.name} className="px-4 py-8 align-top">
                    <div className="flex flex-col gap-4 justify-between">
                      <div>
                        <div className="flex items-baseline gap-1">
                          <span className="text-2xl font-semibold text-foreground">{plan.price}</span>
                          <span>/ month</span>
                        </div>
                        <p className="mt-2 text-sm text-muted-foreground">{plan.description}</p>
                      </div>
                      <div className="mt-4 flex flex-col gap-2">
                        <Button type="button" block variant={plan.popular ? 'solid' : 'default'}>
                          Get started
                        </Button>
                      </div>
                    </div>
                  </td>
                ))}
              </tr>

              {featureGroups.map((group, groupIndex) => (
                <Fragment key={group.name}>
                  <tr>
                    <th
                      scope="colgroup"
                      colSpan={4}
                      className={classNames(
                        'px-4 pb-4 text-left text-base font-semibold text-primary',
                        groupIndex === 0 ? 'pt-0' : 'pt-8',
                      )}
                    >
                      {group.name}
                    </th>
                  </tr>

                  {group.rows.map((row, rowIndex) => (
                    <tr
                      key={row.label}
                      className={classNames(
                        rowIndex === group.rows.length - 1 ? '' : 'border-b',
                      )}
                    >
                      <th scope="row" className="px-4 py-4 text-left text-sm font-medium text-foreground">
                        <span className="inline-flex items-center gap-1.5">
                          {row.label}
                          <Tooltip title={row.tooltip} tabIndex={0} aria-label={`About ${row.label}`}>
                            <PiInfo aria-hidden="true" className="text-base" />
                          </Tooltip>
                        </span>
                      </th>
                      {[row.starter, row.team, row.enterprise].map((cell, cellIndex) => (
                        <td key={cellIndex} className="px-4 py-4">
                          <div className="flex items-center justify-center gap-1.5 text-center">
                            <CellContent cell={cell} />
                          </div>
                        </td>
                      ))}
                    </tr>
                  ))}
                </Fragment>
              ))}
            </tbody>

            <tfoot>
              <tr>
                <th scope="row">
                  <span className="sr-only">Choose your plan</span>
                </th>
                {plans.map((plan) => (
                  <td key={plan.name} className="px-4 pt-8 pb-8">
                    <div className="flex flex-col gap-2">
                      <Button type="button" block variant={plan.popular ? 'solid' : 'default'}>
                        Get started
                      </Button>
                    </div>
                  </td>
                ))}
              </tr>
            </tfoot>
          </table>
        </div>
      </Container>
    </section>
  );
}

Pricing 04

Preview
npx nateui@latest add PricingSliderPlanCard
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Slider from '@/components/ui/Slider';
import Tag from '@/components/ui/Tag';
import Tooltip from '@/components/ui/Tooltip';
import { PiCheckCircleFill, PiInfo } from 'react-icons/pi';

type Tier = {
  pillLabel: string;
  name: string;
  price: number | null;
  description: string;
  live: string;
  staging: string;
  cta: string;
};

const tiers: Tier[] = [
  {
    pillLabel: '1 Website',
    name: 'Solo',
    price: 8,
    description: 'For a single personal project.',
    live: '1 Live Website',
    staging: '1 Staging Website',
    cta: 'Buy Now',
  },
  {
    pillLabel: '5 Websites',
    name: 'Starter',
    price: 19,
    description: 'For a small handful of client sites.',
    live: '5 Live Websites',
    staging: '5 Staging Websites',
    cta: 'Buy Now',
  },
  {
    pillLabel: '10 Websites',
    name: 'Studio',
    price: 34,
    description: 'For a growing freelance practice.',
    live: '10 Live Websites',
    staging: '10 Staging Websites',
    cta: 'Buy Now',
  },
  {
    pillLabel: '20 Websites',
    name: 'Agency',
    price: 59,
    description: 'For agencies running many active builds.',
    live: '20 Live Websites',
    staging: '20 Staging Websites',
    cta: 'Buy Now',
  },
  {
    pillLabel: '40 Websites',
    name: 'Business',
    price: 99,
    description: 'For teams managing sites at scale.',
    live: '40 Live Websites',
    staging: '40 Staging Websites',
    cta: 'Buy Now',
  },
  {
    pillLabel: 'Unlimited Websites',
    name: 'Unlimited',
    price: null,
    description: 'For agencies that need it all, no limits.',
    live: 'Unlimited live websites',
    staging: 'Unlimited staging websites',
    cta: 'Contact sales',
  },
];

const sliderMarks = [
  { value: 0, label: '1' },
  { value: 1, label: '5' },
  { value: 2, label: '10' },
  { value: 3, label: '20' },
  { value: 4, label: '40' },
  { value: 5, label: 'More' },
];

export default function PricingSliderPlanCard() {
  const [tierIndex, setTierIndex] = useState(0);
  const tier = tiers[tierIndex];

  return (
    <section aria-labelledby="pricing-slider-plan-card-title" className="w-full">
      <div className="mx-auto max-w-sm">
         <h4 id="pricing-slider-plan-card-title" className="text-lg font-semibold">
          Choose your plan by number of websites
        </h4>
        <div className="mt-4 px-4">
          <Slider
            value={tierIndex}
            onChange={(value) => setTierIndex(value)}
            min={0}
            max={5}
            step={1}
            marks={sliderMarks}
            thumbAriaLabel="Number of websites"
          />
        </div>

        <Card
          className="mt-12"
          bodyClass="text-center p-8"
        >
          <Tag>{tier.name}</Tag>

          <div className="mt-4 flex items-end justify-center gap-1.5">
            <span className="text-4xl font-bold text-foreground">
              {tier.price === null ? 'Custom' : `$${tier.price}`}
            </span>
            {tier.price !== null && (
              <div className="mt-1 text-start">
                /month
              </div>
            )}
          </div>

          <p className="mt-2 text-muted-foreground">{tier.description}</p>

          <ul className={
            `mt-4 space-y-2 text-start max-w-50 mx-auto
              ${tier.price === null ? 'max-w-56' : 'max-w-50'}
            `
          }>
            <li className="flex items-start gap-2">
              <PiCheckCircleFill aria-hidden="true" className="mt-0.5 shrink-0 text-lg text-success" />
              <span>{tier.live}</span>
            </li>
            <li className="flex items-start gap-2">
              <PiCheckCircleFill aria-hidden="true" className="mt-0.5 shrink-0 text-lg text-success" />
              <span>{tier.staging}</span>
            </li>
            <li className="flex items-start gap-2">
              <PiCheckCircleFill aria-hidden="true" className="mt-0.5 shrink-0 text-lg text-success" />
              <span className="inline-flex items-center gap-1.5">
                Every core feature
                <Tooltip
                  title="No feature gating between plans - every tier ships the full toolkit."
                  tabIndex={0}
                  aria-label="About core features"
                >
                  <PiInfo aria-hidden="true" className="text-base text-muted-foreground" />
                </Tooltip>
              </span>
            </li>
            <li className="flex items-start gap-2">
              <PiCheckCircleFill aria-hidden="true" className="mt-0.5 shrink-0 text-lg text-success" />
              <span>Ongoing updates</span>
            </li>
            <li className="flex items-start gap-2">
              <PiCheckCircleFill aria-hidden="true" className="mt-0.5 shrink-0 text-lg text-success" />
              <span>Cancel or switch anytime</span>
            </li>
          </ul>

          <Button type="button" block variant="solid" className="mt-6">
            {tier.cta}
          </Button>
        </Card>
      </div>
    </section>
  );
}

Pricing 05

Preview
npx nateui@latest add PricingUpgradeProCard
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Radio from '@/components/ui/Radio';
import Tag from '@/components/ui/Tag';
import IconFrame from '@/components/composites/IconFrame';
import classNames from '@/utils/classNames';
import {
  PiArchive,
  PiFolderPlus,
  PiHeadset,
  PiMonitor,
  PiUsersThree,
} from 'react-icons/pi';

const assetBase = 'https://statics.nateui.com/img';

type Feature = {
  icon: typeof PiUsersThree;
  label: string;
};

const features: Feature[] = [
  { icon: PiUsersThree, label: 'Unlimited team members' },
  { icon: PiArchive, label: 'Unlimited storage space' },
  { icon: PiFolderPlus, label: 'Create unlimited projects' },
  { icon: PiMonitor, label: 'Advanced permission controls' },
  { icon: PiHeadset, label: 'Priority support' },
];

type BillingCycle = 'yearly' | 'monthly';

type BillingOption = {
  label: string;
  price: string;
  detail: string;
  badge?: string;
};

const billingOptions: Record<BillingCycle, BillingOption> = {
  yearly: {
    label: 'Yearly',
    price: '$15/month',
    detail: 'Renews once a year',
    badge: 'Save 25%',
  },
  monthly: {
    label: 'Monthly',
    price: '$20/month',
    detail: 'Renews every month',
  },
};

export default function PricingUpgradeProCard() {
  const [billingCycle, setBillingCycle] = useState<BillingCycle>('yearly');

  return (
    <section aria-labelledby="pricing-upgrade-pro-title" className="flex w-full justify-center">
      <Card 
        className="w-full max-w-sm text-center"
        bodyClass="p-8"
      >
        <div className="relative mx-auto h-20 w-20">
          <span
            aria-hidden="true"
            className="absolute inset-0 m-auto h-16 w-16 rotate-12 rounded-lg border bg-muted"
          />
          <span
            aria-hidden="true"
            className="absolute inset-0 m-auto h-16 w-16 -rotate-12 rounded-lg border bg-muted"
          />
          <IconFrame
            size={56}
            className="absolute inset-0 m-auto shadow-card"
          >
            <img
              src={`${assetBase}/thumbs/plans/pro.svg`}
              alt=""
              className="h-10 w-10 object-contain"
            />
          </IconFrame>
        </div>

        <h4 id="pricing-upgrade-pro-title" className="mt-4 text-xl font-semibold text-foreground">
          More room to grow with Pro
        </h4>
        <p className="mt-2 text-muted-foreground">
          Upgrade for higher limits, deeper controls, and support that keeps up with your team.
        </p>

        <div className="mt-8 text-left">
          <span className="font-semibold text-foreground">What&apos;s included:</span>
          <ul className="mt-3 space-y-3">
            {features.map(({ icon: Icon, label }) => (
              <li key={label} className="flex items-center gap-3">
                <IconFrame variant="thick" size={36}>
                  <Icon aria-hidden="true" className="text-lg text-foreground" />
                </IconFrame>
                <span className="font-medium text-foreground">{label}</span>
              </li>
            ))}
          </ul>
        </div>

        <Radio.Group
          vertical
          value={billingCycle}
          onChange={(value) => setBillingCycle(value as BillingCycle)}
          className="mt-8 w-full"
        >
          {(Object.entries(billingOptions) as [BillingCycle, BillingOption][]).map(
            ([key, option]) => (
              <Radio
                key={key}
                value={key}
                className={classNames(
                  'w-full rounded-xl border p-4',
                  billingCycle === key && 'border-primary ring',
                )}
              >
                <span className="flex flex-1 items-center justify-between gap-2">
                  <span className="text-start">
                    <span className="flex items-baseline gap-1.5">
                      <span className="font-semibold text-foreground">
                        {option.label}
                      </span>
                      <span>({option.price})</span>
                    </span>
                    <span className="block text-xs text-muted-foreground">{option.detail}</span>
                  </span>
                  {option.badge && (
                    <Tag className="border-0 bg-primary-soft text-primary">{option.badge}</Tag>
                  )}
                </span>
              </Radio>
            ),
          )}
        </Radio.Group>

        <Button type="button" block variant="solid" className="mt-8">
          Upgrade now
        </Button>
        <Button
          type="button"
          variant="link"
          className="mt-4 underline underline-offset-2"
        >
          Keep using free plan
        </Button>
      </Card>
    </section>
  );
}

Pricing 06

Preview
npx nateui@latest add PricingPlanUpgradeCards
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Segment from '@/components/ui/Segment';
import Tag from '@/components/ui/Tag';
import IconFrame from '@/components/composites/IconFrame';
import classNames from '@/utils/classNames';
import {
  PiCheck,
  PiStarFill
} from 'react-icons/pi';

const assetBase = 'https://statics.nateui.com/img';

type BillingCycle = 'monthly' | 'yearly';

type Plan = {
  name: string;
  description: string;
  iconSrc: string;
  monthlyPrice: number;
  features: string[];
  current?: boolean;
  popular?: boolean;
};

const plans: Plan[] = [
  {
    name: 'Essentials',
    description: 'Everything you need to get started.',
    iconSrc: `${assetBase}/thumbs/plans/basic.svg`,
    monthlyPrice: 79,
    current: true,
    features: [
      'Access to core features',
      'Real-time usage insights',
      'Up to 10 team members',
      '20 GB data storage',
      'Standard email support',
    ],
  },
  {
    name: 'Pro',
    description: 'Advanced tools for growing teams.',
    iconSrc: `${assetBase}/thumbs/plans/standard.svg`,
    monthlyPrice: 149,
    popular: true,
    features: [
      'Everything in Essentials',
      '200+ integrations',
      'Advanced analytics & reporting',
      'Up to 20 team members',
      '40 GB data storage',
      'Priority email support',
    ],
  },
];

export default function PricingPlanUpgradeCards() {
  const [cycle, setCycle] = useState<BillingCycle>('monthly');
  const yearly = cycle === 'yearly';

  return (
    <section aria-labelledby="pricing-plan-upgrade-title" className="w-full">
      <div className="mx-auto max-w-3xl">
      <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
        <div className="flex items-start">
          <div>
            <h3 id="pricing-plan-upgrade-title" className="text-xl font-semibold">
              Choose a plan that grows with you
            </h3>
            <p className="mt-1 text-muted-foreground">
              Unlock more insights, more data, and more possibilities.
            </p>
          </div>
        </div>

        <Segment
          value={cycle}
          onChange={(value) => setCycle(value as BillingCycle)}
          aria-label="Billing cycle"
        >
          <Segment.Item value="monthly">Monthly</Segment.Item>
          <Segment.Item value="yearly">Yearly</Segment.Item>
        </Segment>
      </div>

      <div className="mt-8 grid grid-cols-1 gap-4 md:grid-cols-2">
        {plans.map((plan) => {
          const price = yearly ? Math.round((plan.monthlyPrice * 12)) : plan.monthlyPrice;

          return (
            <Card
              key={plan.name}
              className={plan.popular ? 'border-primary ring' : undefined}
              bodyClass="flex h-full flex-col"
            >
              <div className="flex items-start justify-between gap-2">
                <IconFrame size={40}>
                  <img src={plan.iconSrc} alt="" className="h-6 w-6 object-contain" />
                </IconFrame>
                {plan.popular && (
                  <Tag
                    className="border-0 bg-primary-soft text-primary gap-1"
                  >
                    <PiStarFill aria-hidden="true" className="text-primary" />
                    <span>Most popular</span>
                  </Tag>
                )}
              </div>

              <h4 className="mt-4 text-xl font-semibold text-foreground">{plan.name}</h4>
              <p className="mt-1 text-muted-foreground">{plan.description}</p>

              <div className="mt-4 flex items-baseline gap-1.5">
                <span className="text-3xl font-bold text-foreground">${price}</span>
                <span>/
                  {yearly ? 'year' : 'month'}
                </span>
              </div>

              <ul className="mt-8 flex-1 space-y-3">
                {plan.features.map((feature) => (
                  <li key={feature} className="flex items-center gap-3">
                    <span className="flex h-5 w-5 items-center justify-center rounded-md bg-muted">
                      <PiCheck aria-hidden="true" className="shrink-0 text-foreground" />
                    </span>
                    <span className="text-foreground">{feature}</span>
                  </li>
                ))}
              </ul>

              <Button
                type="button"
                block
                variant={plan.current ? 'default' : 'solid'}
                disabled={plan.current}
                className={classNames(
                  'mt-8',
                  plan.popular && 'bg-gradient-to-br from-primary to-primary/70',
                )}
              >
                {plan.current ? 'Your current plan' : `Upgrade to ${plan.name}`}
              </Button>
            </Card>
          );
        })}
      </div>
      </div>
    </section>
  );
}

Pricing 07

Preview
npx nateui@latest add PricingCreditPackSelector
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Tag from '@/components/ui/Tag';
import IconFrame from '@/components/composites/IconFrame';
import classNames from '@/utils/classNames';
import { PiCheck, PiCheckCircle, PiInfo, PiSparkleFill } from 'react-icons/pi';

type CreditPack = {
  name: string;
  credits: number;
  usageLabel: string;
  price: number;
  tagline: string;
  badge?: string;
};

const packs: CreditPack[] = [
  {
    name: 'Lite Pack',
    credits: 150,
    usageLabel: 'Occasional use',
    price: 79,
    tagline: 'Enough headroom for a side project.',
  },
  {
    name: 'Plus Pack',
    credits: 400,
    usageLabel: 'Everyday use',
    price: 179,
    tagline: 'Room to scale without watching the meter.',
    badge: 'Most Popular',
  },
  {
    name: 'Max Pack',
    credits: 800,
    usageLabel: 'Nonstop use',
    price: 319,
    tagline: 'Built for teams running at full throttle.',
    badge: 'Best Rate',
  },
];

const highlights = [
  {
    title: 'No request caps',
    description: 'Send as much as your workflow needs, with no daily ceiling.',
  },
  {
    title: 'Real-time processing',
    description: 'Requests complete in seconds - nothing sits in a queue.',
  },
  {
    title: 'Direct team access',
    description: 'A shared channel straight to the people who build it.',
  },
];

export default function PricingCreditPackSelector() {
  const [selectedIndex, setSelectedIndex] = useState(0);
  const selectedPack = packs[selectedIndex];

  return (
    <section aria-labelledby="pricing-credit-pack-title" className="w-full">
      <div className="mx-auto grid max-w-4xl grid-cols-1 gap-8 md:grid-cols-2">
        <div>
          <Tag>Credit Packs</Tag>

          <h4 id="pricing-credit-pack-title" className="mt-4 text-xl font-semibold">
            Credits that work your way
          </h4>
          <p className="mt-2 text-muted-foreground">
            Buy credits once and use them whenever you need them. No subscriptions, no expiry
            games.
          </p>

          <div className="mt-8 space-y-2">
            {packs.map((pack, index) => {
              const isSelected = index === selectedIndex;

              return (
                <button
                  key={pack.name}
                  type="button"
                  onClick={() => setSelectedIndex(index)}
                  aria-pressed={isSelected}
                  className={classNames(
                    'flex w-full items-center justify-between gap-4 rounded-card border bg-card p-4 text-left transition-colors',
                    isSelected ? 'border-primary ring' : 'hover:bg-muted',
                  )}
                >
                  <span>
                    <span className="block font-semibold text-foreground">{pack.name}</span>
                    <span className="mt-1 block">
                      {pack.credits} Credits &bull; {pack.usageLabel}
                    </span>
                  </span>
                  {isSelected ? (
                    <span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary">
                      <PiCheck aria-hidden="true" className="text-xs text-primary-foreground" />
                    </span>
                  ) : (
                    <span className="h-5 w-5 shrink-0 rounded-full border-2 border" />
                  )}
                </button>
              );
            })}
          </div>
        </div>

        <Card bodyClass="p-8">
          <div className="flex items-start justify-between gap-2">
            <div className="flex items-center gap-3">
              <IconFrame size={40}>
                <PiSparkleFill aria-hidden="true" className="text-lg text-foreground" />
              </IconFrame>
              <h4 className="text-xl font-semibold text-foreground">
                {selectedPack.credits} Credits
              </h4>
            </div>
          </div>

          <div className="mt-4 flex items-baseline gap-1.5">
            <span className="text-3xl font-bold text-foreground">
              ${selectedPack.price}
            </span>
            <span>/ one time</span>
          </div>

          <Button type="button" block variant="solid" className="mt-4">
            Get {selectedPack.credits} Credits
          </Button>
          <p className="mt-3 text-center text-sm text-muted-foreground">{selectedPack.tagline}</p>

          <div className="mt-8 border-t pt-8">
            <span className="text-sm font-semibold text-foreground">Highlights:</span>
            <ul className="mt-3 space-y-3">
              {highlights.map((item) => (
                <li key={item.title} className="flex items-start gap-3">
                  <PiCheckCircle aria-hidden="true" className="mt-0.5 shrink-0 text-lg text-foreground" />
                  <span>
                    <span className="block text-sm font-medium text-foreground">{item.title}</span>
                    <span className="text-sm text-muted-foreground">{item.description}</span>
                  </span>
                </li>
              ))}
            </ul>
          </div>

          <div className="mt-8 flex items-start gap-2 border-t pt-4 text-xs text-muted-foreground">
            <PiInfo aria-hidden="true" className="mt-0.5 shrink-0" />
            <span>
              Packs add usage headroom only &mdash; plan-level features stay tied to your
              subscription tier.
            </span>
          </div>
        </Card>
      </div>
    </section>
  );
}