Skip to content
oks-ui

Marketing

Pricing table

The pricing section done as a real feature comparison, not four identical bullet lists: each plan has its own checklist of included/excluded features (real Check/X icons, not text alone), a live Bill monthly/Bill annually switch that recomputes every price and reveals a "Save $N" badge on the recommended tier, and that tier promoted with a fixed dark card that stays legible in both themes.

pricing-table.tsx

How it's built

1

The Bill monthly/Bill annually switch is a real `FormFieldSet type="switch"`, controlled (`checked`/`onChange`) — both flanking labels and the "Save $N" badge all derive from the same `isAnnual` state, so they can never disagree.

2

Every feature row uses a real `CheckIcon`/`XIcon` colored via a wrapping `<span>` — the icons draw with `stroke="currentColor"` and take no color prop themselves, so the color has to come from an ancestor's text color.

3

The featured plan's dark card uses a fixed hex (`bg-[#241206]`), not an oks-ui semantic token like `--oks-color-primary-950` — that token is *designed* to flip to a near-white shade in dark mode, which would wash the card's white text out to near-invisible instead of staying a deliberately dark, promoted card in both themes.

4

The grid needs its own `@container` — `@2xl:grid-cols-4` has nothing to evaluate against without an ancestor establishing container-type, so it silently never applies and the plans stay stacked in one column regardless of how wide the page is.

Source

One file · copy & paste
pricing-table.tsx
import { useState } from "react";
import { Button } from "oks-ui/button";
import { Card, CardBody } from "oks-ui/card";
import { Chip } from "oks-ui/chip";
import { FormFieldSet } from "oks-ui/form-field-set";
import { PageTitle } from "oks-ui/page-title";
import { CheckIcon, XIcon } from "../../showcase/icons";

const plans = [
  {
    name: "Intro",
    monthly: 9,
    annual: 90,
    featured: false,
    features: [
      { label: "Upload video up to 720p", included: true },
      { label: "Attachment & post scheduling", included: false },
      { label: "Set your own rates", included: false },
      { label: "Exclusive deals", included: false },
      { label: "Advanced statistics", included: false },
    ],
  },
  {
    name: "Base",
    monthly: 15,
    annual: 150,
    featured: false,
    features: [
      { label: "Upload video in HD", included: true },
      { label: "Attachment & post scheduling", included: true },
      { label: "Set your own rates", included: false },
      { label: "Exclusive deals", included: false },
      { label: "Advanced statistics", included: false },
    ],
  },
  {
    name: "Pro",
    monthly: 18,
    annual: 176,
    featured: true,
    features: [
      { label: "Upload video in HD", included: true },
      { label: "Attachment & post scheduling", included: true },
      { label: "Set your own rates", included: true },
      { label: "Exclusive deals", included: true },
      { label: "Advanced statistics", included: false },
    ],
  },
  {
    name: "Enterprise",
    monthly: 39,
    annual: 390,
    featured: false,
    features: [
      { label: "Upload video in 4K", included: true },
      { label: "Attachment & post scheduling", included: true },
      { label: "Set your own rates", included: true },
      { label: "Exclusive deals", included: true },
      { label: "Advanced statistics", included: true },
    ],
  },
];

export function PricingTable() {
  const [isAnnual, setIsAnnual] = useState(false);

  return (
    // @container so the @2xl:grid-cols-4 below has a container to query —
    // without an ancestor establishing container-type, a container-query
    // utility has nothing to evaluate against and silently never applies.
    <div className="@container flex flex-col items-center gap-8 px-10 pt-20 pb-40 text-center @3xl:px-12">
      <PageTitle
        as="h2"
        title="The right plan for your business"
        subtitle="Straightforward pricing that scales with your team — switch between monthly and annual billing anytime."
        classNames={{
          base: "flex-col items-center gap-2",
          title: "text-3xl font-bold tracking-tight text-ink",
          subtitle: "max-w-lg text-[0.9rem] text-ink-muted",
        }}
      />

      <div className="flex items-center gap-3">
        <span
          className={`whitespace-nowrap text-[0.9rem] font-medium ${!isAnnual ? "text-ink" : "text-ink-muted"}`}
        >
          Bill monthly
        </span>
        <FormFieldSet
          type="switch"
          name="billingPeriod"
          label={
            <span className="sr-only">Bill annually instead of monthly</span>
          }
          checked={isAnnual}
          onChange={(v: unknown) => setIsAnnual(Boolean(v))}
        />
        <span
          className={`whitespace-nowrap text-[0.9rem] font-medium ${isAnnual ? "text-ink" : "text-ink-muted"}`}
        >
          Bill annually
        </span>
      </div>

      <div className="grid w-full gap-5 @2xl:grid-cols-4">
        {plans.map((plan) => (
          <Card
            key={plan.name}
            shadow={plan.featured ? "lg" : "sm"}
            // A fixed hex, not an oks-ui semantic token — the featured
            // card's white text needs the background to stay dark in
            // BOTH themes. A token like --oks-color-primary-950 flips to a
            // near-white shade in dark mode by design, which would wash
            // the white text out to near-invisible.
            className={plan.featured ? "bg-[#241206]" : ""}
          >
            <CardBody className="flex flex-col gap-5 p-6 text-left">
              <div className="flex items-center justify-between gap-2">
                <h3
                  className={`text-[0.95rem] font-semibold ${plan.featured ? "text-white" : "text-ink"}`}
                >
                  {plan.name}
                </h3>
                {plan.featured && isAnnual ? (
                  <Chip size="sm" variant="solid" color="primary">
                    Save ${plan.monthly * 12 - plan.annual}
                  </Chip>
                ) : null}
              </div>

              <div className="flex flex-col gap-3">
                {plan.features.map((f) => (
                  <div key={f.label} className="flex items-start gap-2">
                    <span
                      className={`shrink-0 ${
                        f.included
                          ? plan.featured
                            ? "text-[color:var(--oks-color-success-400)]"
                            : "text-[color:var(--oks-color-success-700)]"
                          : plan.featured
                            ? "text-white/30"
                            : "text-ink-faint"
                      }`}
                    >
                      {f.included ? (
                        <CheckIcon size={16} />
                      ) : (
                        <XIcon size={16} />
                      )}
                    </span>
                    <span
                      className={`text-[0.85rem] ${
                        plan.featured
                          ? f.included
                            ? "text-white"
                            : "text-white/60"
                          : f.included
                            ? "text-ink-soft"
                            : "text-ink-faint"
                      }`}
                    >
                      {f.label}
                    </span>
                  </div>
                ))}
              </div>

              <p className={plan.featured ? "text-white" : "text-ink"}>
                <span className="text-[1.9rem] font-extrabold tracking-tight">
                  ${isAnnual ? plan.annual : plan.monthly}
                </span>
                <span
                  className={`text-[0.85rem] ${plan.featured ? "text-white/70" : "text-ink-muted"}`}
                >
                  /{isAnnual ? "year" : "month"}
                </span>
              </p>

              <Button
                fullWidth
                radius="full"
                color="primary"
                variant={plan.featured ? "solid" : "soft"}
              >
                {plan.featured ? "Try 1 month" : "Choose plan"}
              </Button>
            </CardBody>
          </Card>
        ))}
      </div>
    </div>
  );
}

More patterns

App screens
Analytics dashboardA full fintech app screen — collapsible sidebar, header search, wallet cards, an earnings chart, savings goals, and a transactions table.
Auth
Sign-in screenA split-screen sign-in with a feature panel, Google SSO, and remember-me.
App screens
Settings panelA real settings screen — top tabs, two-column sections, and a sortable, selectable billing history table.