Why pricing pages fall flat

A common way to ship a first pricing page is to copy a design you like, swap the colors, and write the plan names five minutes before launch. It works, but it misses two things:

  • People don’t buy plans; they buy outcomes.
  • Your pricing page is a narrative, not a grid.

Below is a process to plan the story, draft honest copy, and ship the page with Laravel + Tailwind.

Step 1 — Write the headline like a promise

If your H1 reads “Simple, transparent pricing”, it says nothing. Try something like this instead:

“Ship release notes your customers actually read — from $12/mo.”

It’s specific, it names the outcome, and it anchors price early. For the page’s search title and description, see the SaaS SEO checklist.

Step 2 — Wireframe the narrative

Sketch this order (pen and paper works):

  1. Headline + subhead stating the outcome and time‑to‑value.
  2. Toggle: Monthly vs Annual (show annual savings in real dollars).
  3. Plan cards with one‑sentence summaries (“For solo founders shipping weekly updates”).
  4. Social proof, if you have it: a real quote from a customer who agreed to be quoted, or a true usage number. Leave it out rather than invent it.
  5. An FAQ block that answers obvious objections.

Keep the first screen decision‑friendly. A detailed comparison table can live further down for people who want it.

Step 3 — Draft copy that sounds like a person

You don’t need flowery language. You need clarity. Use phrases customers actually say on calls. If they say “I don’t have time to design emails,” your copy might read “Send on‑brand emails without designing them.” Only put a number in the copy (“save 3 hours a week”) if you can back it up.

A simple formula per plan:

  • Who it’s for
  • What it gets them (1–2 outcomes)
  • One specific guardrail (“Up to 3 team members”)

For finding the price itself, see Pricing Discovery Interviews for B2B SaaS.

Step 4 — Build it in Laravel + Tailwind

Here’s a trimmed Blade snippet. It uses Alpine.js for the billing toggle (Alpine ships with Livewire 3; otherwise add it to your page) and computes the annual saving in dollars from the plan data, so the badge can’t drift out of sync with the prices.

{{-- resources/views/pricing.blade.php --}}
@php
    $plans = [
        ['key' => 'starter',  'name' => 'Starter',  'blurb' => 'For solo builders', 'monthly' => 12, 'annual' => 120],
        ['key' => 'growth',   'name' => 'Growth',   'blurb' => 'For small teams',   'monthly' => 29, 'annual' => 290],
        ['key' => 'business', 'name' => 'Business', 'blurb' => 'For steady teams',  'monthly' => 79, 'annual' => 790],
    ];
@endphp

<div x-data="{ annual: false }">
    <div class="mb-6 flex items-center gap-3">
        <span class="text-sm">Monthly</span>
        <button type="button" role="switch" :aria-checked="annual.toString()" @click="annual = !annual"
                class="relative h-6 w-12 rounded-full transition" :class="annual ? 'bg-orange-500' : 'bg-gray-300'">
            <span class="sr-only">Bill annually</span>
            <span class="absolute left-1 top-1 h-4 w-4 rounded-full bg-white shadow transition"
                  :class="annual && 'translate-x-6'"></span>
        </button>
        <span class="text-sm">Annual</span>
    </div>

    <div class="grid gap-4 md:grid-cols-3">
        @foreach ($plans as $plan)
            <div @class([
                'rounded-xl border border-gray-200 p-6',
                'ring-2 ring-orange-400' => $plan['key'] === 'growth',
            ])>
                <div class="flex items-baseline justify-between">
                    <h3 class="text-xl font-semibold">{{ $plan['name'] }}</h3>
                    @if ($plan['key'] === 'growth')
                        <span class="rounded-full bg-orange-100 px-2 py-0.5 text-xs text-orange-700">Most popular</span>
                    @endif
                </div>
                <p class="mt-1 text-gray-600">{{ $plan['blurb'] }}</p>

                <div class="mt-3 text-3xl font-semibold">
                    <span x-show="!annual">${{ $plan['monthly'] }}<span class="text-base text-gray-500">/mo</span></span>
                    <span x-show="annual" x-cloak>${{ $plan['annual'] }}<span class="text-base text-gray-500">/yr</span></span>
                </div>
                <p x-show="annual" x-cloak class="mt-1 text-sm text-green-700">
                    Save ${{ $plan['monthly'] * 12 - $plan['annual'] }} a year
                </p>

                <ul class="mt-3 space-y-2 text-sm">
                    <li>Unlimited projects</li>
                    <li>Changelog widget</li>
                    <li>Email sending included</li>
                </ul>

                <a :href="'{{ route('checkout.start', $plan['key']) }}' + (annual ? '?interval=year' : '')"
                   href="{{ route('checkout.start', $plan['key']) }}"
                   class="mt-4 inline-block rounded-lg bg-gray-900 px-4 py-2 text-white">
                    Start {{ $plan['name'] }}
                </a>
            </div>
        @endforeach
    </div>
</div>

checkout.start stands in for your own checkout route; with Laravel Cashier it would look up the Stripe price for the plan and interval and redirect to Checkout. Keep the real prices in one place (config or the database) and render them from there, rather than typing them into the view. Add [x-cloak] { display: none; } to your CSS so the annual prices don’t flash on load.

For styling, lean on Tailwind utility classes you already use in the app. Avoid bespoke CSS where you can; it’s one more surface to maintain.

Step 5 — Measure what matters

Track three things first:

  • Click‑through rate on the primary CTA per plan
  • Annual vs monthly toggle usage
  • Drop‑offs between pricing and checkout

If you want more detail, instrument “compare table opened” and “FAQ opened” events. They show which questions people have before they buy.

Common mistakes

  • Four or more plans with indistinguishable names
  • Pricing hidden behind a demo call for a small‑business product
  • Feature tables that list your database schema instead of outcomes

If you’re earlier in the journey, start with the SaaS Pre‑Launch Checklist and the B2B SaaS Go‑to‑Market Checklist. When billing is wired up, check Stripe + Laravel Subscriptions: 12 Common Gotchas and Fixes.

A quick checklist before you ship

  • One clear promise above the fold
  • Annual savings shown in dollars, and correct
  • Social proof that is real, or none at all
  • 3 FAQs that address real objections
  • Obvious CTA copy (“Start Growth” is clearer than “Get Started”)

Ship a simple version first. You can always redesign once you see how people use it.