Pricing cards

Difficulty S

The clearest everyday illustration of visual hierarchy as a decision – which card leads, which button is primary, and what happens when a page refuses to choose.

Seen in: Every SaaS marketing site – and your placement quiz, where you read this exact pattern's hierarchy unprompted

Emphasise:

Starter

$0 forever

For trying the thing properly.

  • 3 projects
  • Community support
  • Core integrations

Scale

$49 per person / month

For when the audit team calls.

  • Everything in Growth
  • SSO & audit log
  • Dedicated support
  • 99.9% SLA

One card carries the emphasis: accent border, recommendation badge, the only solid button. The other two stay legible but visibly secondary – the reader’s comparison starts where you decided it should.

Teaches

  • Visual hierarchy
  • Primary vs secondary CTA
  • The competing-hierarchy failure

What to notice, in order:

The emphasis is one treatment applied consistently. The featured card gets the accent border, the badge, and the only solid button – three signals agreeing. Everything on the other cards stays fully legible; it just declines to compete. Emphasis isn’t decoration on top of a card, it’s a rank expressed through the same levers every time.

Move the emphasis and the page’s argument changes. Feature Starter and the page says “try before you buy”; feature Scale and it says “we sell to serious teams”. Same data, same layout – hierarchy is the rhetoric.

“All of them” is the control group. Three recommendation badges and three solid buttons is what pricing pages ship when nobody makes the call. It isn’t ugly – that’s why it survives review. It’s undecided, and the buyer inherits the decision.

Responsive behaviour is part of the pattern. The cards form a grid that stacks on narrow screens – on a phone, emphasis has to survive vertically (the featured card still reads featured mid-scroll).

View the source – 97 lines of TypeScript/React, tokens only
import { useState } from 'react';
import '../demos/demos.css';

/*
  Specimen: pricing cards (library #15, S difficulty).
  The interactive twist teaches the lesson: emphasis is a decision, and you
  can move it – or refuse to make it, which is the competing-hierarchy
  failure rendered live. Pattern reconstructed from the near-universal SaaS
  convention; original code (ADR #006).
*/

type Emphasis = 'starter' | 'growth' | 'scale' | 'all';

const TIERS = [
  {
    id: 'starter' as const,
    name: 'Starter',
    price: '$0',
    cadence: 'forever',
    line: 'For trying the thing properly.',
    features: ['3 projects', 'Community support', 'Core integrations'],
    cta: 'Start free',
  },
  {
    id: 'growth' as const,
    name: 'Growth',
    price: '$19',
    cadence: 'per person / month',
    line: 'For teams shipping every week.',
    features: ['Unlimited projects', 'Priority support', 'API access', 'Usage analytics'],
    cta: 'Start 14-day trial',
  },
  {
    id: 'scale' as const,
    name: 'Scale',
    price: '$49',
    cadence: 'per person / month',
    line: 'For when the audit team calls.',
    features: ['Everything in Growth', 'SSO & audit log', 'Dedicated support', '99.9% SLA'],
    cta: 'Talk to sales',
  },
];

export default function PricingCards() {
  const [emphasis, setEmphasis] = useState<Emphasis>('growth');

  return (
    <div>
      <div className="pc-controls" role="group" aria-label="Choose which tier is emphasised">
        <span className="pc-controls-label">Emphasise:</span>
        {(['starter', 'growth', 'scale', 'all'] as const).map((option) => (
          <button
            key={option}
            type="button"
            className="chip"
            aria-pressed={emphasis === option}
            onClick={() => setEmphasis(option)}
          >
            {option === 'all' ? 'All of them (the failure)' : TIERS.find((t) => t.id === option)?.name}
          </button>
        ))}
      </div>

      <div className="pc-row">
        {TIERS.map((tier) => {
          const featured = emphasis === 'all' || emphasis === tier.id;
          return (
            <article key={tier.id} className={`pc-card${featured ? ' is-featured' : ''}`}>
              {featured && emphasis !== 'all' && <span className="badge badge-accent pc-flag">Recommended</span>}
              {featured && emphasis === 'all' && <span className="badge badge-accent pc-flag">Best value!</span>}
              <h4 className="pc-name">{tier.name}</h4>
              <p className="pc-price">
                {tier.price} <small>{tier.cadence}</small>
              </p>
              <p className="pc-line">{tier.line}</p>
              <ul className="pc-features">
                {tier.features.map((feature) => (
                  <li key={feature}>{feature}</li>
                ))}
              </ul>
              <button type="button" className={featured ? 'btn btn-primary' : 'btn btn-secondary'}>
                {tier.cta}
              </button>
            </article>
          );
        })}
      </div>

      <p className="pc-note" aria-live="polite">
        {emphasis === 'all'
          ? 'Three "recommended" cards, three primary buttons – every element promoted, so none is. This is what most un-designed pricing pages actually ship.'
          : 'One card carries the emphasis: accent border, recommendation badge, the only solid button. The other two stay legible but visibly secondary – the reader’s comparison starts where you decided it should.'}
      </p>
    </div>
  );
}