EmpireUI
Get Pro
← Blog7 min read#tailwind-css#pricing-page#saas-ui

Tailwind Pricing Page: SaaS Tier Comparison Design

Build a SaaS pricing page with Tailwind CSS — tier cards, toggle billing, highlight rings, and responsive stacking that actually converts without drowning in utility classes.

SaaS pricing page layout with tier cards on a dark background showing monthly and annual billing options

Why Pricing Pages Break Most Tailwind Setups

Honestly, a pricing page is the one UI that exposes every gap in your design system. You'll have three or four cards sitting side-by-side, one of them needs to look "featured" without screaming, and the whole thing has to collapse gracefully on mobile. That's a lot of moving parts.

Most tutorials show you a single card in isolation. That's useless. The real challenge is the relationship between cards — the spacing, the alignment of feature rows, the way a highlight ring on the "Pro" tier affects the surrounding layout without pushing adjacent cards around.

This guide builds a full pricing section in Tailwind v4.0.2 with a monthly/annual toggle, three tier cards, a featured card variant, and responsive stacking. No component framework assumptions — just TSX and utility classes you can drop into any project.

Data Structure and Component Architecture

Before writing a single className, define your tier data shape. Pricing cards share 90% of their structure — only the values differ. Hardcoding each card separately is how you end up with three slightly different implementations that drift apart every time copy changes.

Keep a plans array with typed objects. Each plan carries a name, monthlyPrice, annualPrice, description, features array, a featured boolean, and a cta label. The featured flag is the only thing that forks the visual rendering.

One parent PricingSection component holds the toggle state. It passes isAnnual and the resolved price down to each PricingCard. That's it. Don't let the card manage billing state — it doesn't need to know.

Building the Billing Toggle with Tailwind

The toggle is a small thing that's annoying to get right. You want a pill-shaped switch, an animated thumb, and a label that shows the discount badge when annual is selected. All with zero JavaScript libraries.

Here's a clean implementation using a controlled checkbox and Tailwind's peer modifier pattern:

type BillingToggleProps = {
  isAnnual: boolean;
  onChange: (val: boolean) => void;
};

export function BillingToggle({ isAnnual, onChange }: BillingToggleProps) {
  return (
    <div className="flex items-center gap-3 text-sm font-medium">
      <span className={isAnnual ? "text-zinc-400" : "text-white"}>Monthly</span>
      <button
        role="switch"
        aria-checked={isAnnual}
        onClick={() => onChange(!isAnnual)}
        className={[
          "relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200",
          isAnnual ? "bg-violet-600" : "bg-zinc-700",
        ].join(" ")}
      >
        <span
          className={[
            "inline-block h-4 w-4 rounded-full bg-white shadow transition-transform duration-200",
            isAnnual ? "translate-x-6" : "translate-x-1",
          ].join(" ")}
        />
      </button>
      <span className={isAnnual ? "text-white" : "text-zinc-400"}>
        Annual
        {isAnnual && (
          <span className="ml-2 rounded-full bg-violet-500/20 px-2 py-0.5 text-xs text-violet-300">
            Save 20%
          </span>
        )}
      </span>
    </div>
  );
}

The translate-x-6 / translate-x-1 pair gives you a 20px thumb travel across the 44px track. That 8px gap between thumb edge and track wall is intentional — it prevents the thumb from looking crammed. Tailwind's duration-200 on both the track color and thumb position means they animate in sync.

Pricing Card Layout and the Featured Ring Trick

The featured card needs to stand out without breaking the row alignment. A common mistake is adding padding-top or margin-top: -8px to make it taller. That shifts the feature list rows out of vertical alignment across cards, which looks sloppy on wide screens.

Better approach: keep all cards the same height and use a combination of ring, shadow, and scale to create visual prominence. Tailwind makes this trivial with the ring-2 ring-violet-500 and scale-[1.03] utilities. The scale is subtle — 3% — but it reads as "this one matters".

For the card grid itself, use grid-cols-1 md:grid-cols-3 with a gap-6. On the featured card, add z-10 relative so the scaled card sits above its siblings without clipping. You also want self-stretch on non-featured cards so feature rows align when cards are equal height, which is why Tailwind container queries are worth learning for layouts like this — they let the card itself respond to its column width rather than the viewport.

Feature Row Alignment Across Tier Cards

This is where most pricing pages quietly fall apart. If your "Starter" card has 5 features and "Pro" has 8, you can't just render a list and call it done. The rows won't align across cards, and it looks like three separate components thrown next to each other.

One pattern: define a master feature list. Each plan has a set of feature keys. Render all features on every card, but mark absent features with a muted strikethrough or a grey minus icon. It communicates limits clearly and keeps rows vertically locked.

Another pattern — if you don't want absent features visible — is to pad each card's feature list to the length of the longest list with invisible placeholder rows. Less semantically meaningful but visually cleaner. Which you pick depends on whether you want to emphasize what lower tiers lack. For SaaS, usually you do. Check out how Tailwind component patterns handles repeating structural layouts like this.

Dark Mode and OKLCH Color Tokens for Tier Accents

Dark pricing pages are the norm for SaaS. Light ones feel dated. But dark mode in Tailwind v4.0.2 isn't just slapping dark: prefixes everywhere — if you're using CSS custom properties for your tier accent colors, you can switch entire palettes with a single class toggle.

For the featured card's gradient background, avoid opaque fills. Use rgba(255,255,255,0.04) as the card surface with a rgba(139,92,246,0.15) violet tint — it reads as distinct without being garish. You can define these as CSS vars and swap them for a theme toggle implementation if your product has one.

If you're on Tailwind v4 with OKLCH support, the tier accent colors get much easier to manage. Something like oklch(65% 0.22 290) gives you a vivid violet that's perceptually consistent across your UI. Read more on how Tailwind OKLCH colors change the way you define your palette — especially for something like pricing where you need one accent color that works at multiple opacities.

Responsive Stacking and Mobile CTA Placement

On mobile, three cards stacked vertically with the featured one in the middle is a bad experience. Users have to scroll past the "Starter" card to see "Pro", which is likely your conversion target. Reorder the stack on mobile so "Pro" shows first.

Tailwind's order- utilities handle this cleanly. Give the featured card order-first md:order-none and you're done. No media query hacks, no JavaScript reordering.

The CTA button placement also matters on mobile. If it's at the bottom of a tall card, users might not see it without scrolling. Consider a sticky CTA bar that appears below the cards on small screens — a fixed bottom-0 bar with the plan name and price, only visible on mobile via block md:hidden. It's an extra 15 lines of markup but it meaningfully affects conversion. And if you want to add some depth to the section behind the cards, a particles background or subtle glassmorphism layer can make the whole section feel premium without heavy custom CSS — see glassmorphism basics for the foundational technique.

Performance and Accessibility Checklist Before Ship

Pricing pages carry real conversion weight. So they need to be fast and accessible — not as an afterthought, but because broken accessibility directly hurts screen-reader users who are trying to buy your product.

Run through this before deploying: every card needs a visible heading hierarchy (h2 for section, h3 for plan name, h4 for price). The billing toggle must use role="switch" and aria-checked. Feature check icons need aria-label or screen-reader-only text, not just a visual checkmark. The CTA buttons must have unique accessible names — "Get Started" three times in a row is useless for users navigating by button. "Get Started with Pro" is better.

On performance: the pricing section is almost always below the fold. Lazy-load any decorative background effects. Keep the Tailwind output lean by avoiding one-off arbitrary values like w-[347px] — that adds a new class to your bundle for zero design reason. Use the 4px grid. w-80 (320px) or w-96 (384px) is almost always close enough, and your output stays smaller.

FAQ

How do I highlight one pricing card without breaking the row height alignment in Tailwind?

Use scale-[1.03] and ring-2 ring-violet-500 on the featured card instead of negative margins or extra padding. Add z-10 relative so the scaled card sits above siblings. Keep padding identical across all cards so feature rows stay vertically aligned.

What's the right way to handle the annual/monthly price toggle in React with Tailwind?

Hold isAnnual state in the parent pricing section and pass it as a prop to each card. The card just picks isAnnual ? plan.annualPrice : plan.monthlyPrice — no local state needed. Animate the toggle with Tailwind's transition-colors and translate-x utilities on a controlled button with role="switch".

Should I use CSS Grid or Flexbox for the three-column pricing card layout?

Grid. Use grid grid-cols-1 md:grid-cols-3 gap-6. Flexbox works but you lose the ability to control both row and column alignment declaratively. With grid, self-stretch on non-featured cards ensures they fill the row height, keeping feature lists aligned.

How do I reorder pricing cards on mobile so the featured plan appears first?

Add order-first md:order-none to the featured card in Tailwind. On mobile it jumps to the top of the stacked list; on medium screens and above the CSS order is reset to source order, putting it in the middle column.

What accessible markup does a pricing toggle need in Tailwind v4?

The toggle button should have role="switch" and aria-checked={isAnnual}. If you're using a visual-only icon for the thumb, wrap it in a <span aria-hidden="true">. The surrounding labels ("Monthly", "Annual") should be real text — not images or icons — so screen readers can read the current selection in context.

Can I use Tailwind arbitrary values like w-[347px] for pricing card widths?

Technically yes, but don't. Arbitrary values generate a new utility class in your output bundle for each unique value. Stick to the 4px grid spacing scale — w-80 (320px) or w-96 (384px) will cover almost every pricing card width need. Use max-w-sm, max-w-md, or max-w-lg for responsive max-widths instead.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Tailwind Table Component: Responsive Data Tables with OverflowTailwind Responsive Design: Beyond Mobile-First, Container QueriesPricing Card Variants: 7 SaaS Designs That Increase ConversionsDark UI Patterns for SaaS: Navigation, Sidebars, Data Tables