DEV Community

Cover image for We rewrote our pricing page 3 times. Here's what worked.
Russel Dsouza
Russel Dsouza

Posted on • Originally published at applighter.com

We rewrote our pricing page 3 times. Here's what worked.

We rewrote components/pricing-cards.tsx three times in six months. Same product, same Stripe checkout, same three license tiers (single | multiple | enterprise). What changed was the component. The conversion went from ~1.1% to ~3.4%.

This post is the actual diffs and the actual numbers. No theory, no funnel-graph PNGs.

The product

Applighter sells full-stack React Native + Expo templates. Each template has three license tiers stored as rows in a Supabase product_licenses table:

license_type     text check (license_type in ('single','multiple','enterprise'))
price_usd        numeric not null
allowed_users    int
allowed_projects int
is_commercial    boolean
Enter fullscreen mode Exit fullscreen mode

Tiers are correct. The component rendering them was wrong three times.

Version 1: textbook, ~1.1%

{licenses.map(license => (
  <Card>
    <CardTitle>{license.license_type}</CardTitle>
    <div className="text-4xl">${license.price_usd.toFixed(2)}</div>
    <ul>{allFeatures.map(f => <li>{f}</li>)}</ul>
    <BuyButton />
  </Card>
))}
Enter fullscreen mode Exit fullscreen mode

Three identical cards. Long feature matrix. .toFixed(2) on every price for "safety." A weak border on the middle card that wasn't visible in dark mode.

What the data said:

  • 38% of sessions ended without a single pricing_card_focus event
  • Time spent disproportionately on the cheapest card
  • The multiple tier — best per-developer economics — got the least attention

Classic decision-paralysis. Three identical options read as a comparison spreadsheet, not a recommendation.

Version 2: anchor the middle tier, ~1.9%

The fix was one boolean:

function getLicenseHighlight(type: string): boolean {
  return type === "multiple";
}
Enter fullscreen mode Exit fullscreen mode

Wired into the card:

<Card className={isHighlighted ? "border-primary shadow-lg" : ""}>
  {isHighlighted && (
    <Badge className="absolute -top-3 left-1/2 -translate-x-1/2">
      Most Popular
    </Badge>
  )}
  ...
</Card>
Enter fullscreen mode Exit fullscreen mode

And reordered columns left-to-right by price: cheapest, recommended, premium. Buyer's eye now lands on the middle tier first; the others become reference points.

Result: ~1.1% → ~1.9% over four weeks. Almost double from one boolean and a sort.

Version 3: small fixes, ~3.4%

Fix 1: drop trailing .00

The one-liner that lifted conversion the most:

<span className="text-4xl font-bold">
  ${Number(license.price_usd) % 1 === 0
    ? Number(license.price_usd)
    : Number(license.price_usd).toFixed(2)}
</span>
Enter fullscreen mode Exit fullscreen mode

If the price is a whole dollar, render the integer. Otherwise two decimals. $49.00 → $49.

This change, isolated for two weeks holding everything else constant, accounted for roughly half the v2→v3 lift. The most embarrassing finding in our analytics this year.

Fix 2: replace the feature matrix with six bullets per card

<ul className="space-y-3">
  <li><Check /> {license.allowed_users
    ? `Up to ${license.allowed_users} developers`
    : "Unlimited developers"}</li>
  <li><Check /> {license.allowed_projects
    ? `${license.allowed_projects} projects`
    : "Unlimited projects"}</li>
  <li><Check /> {license.is_commercial
    ? "Commercial use included"
    : "Personal use"}</li>
  {features.slice(0, 3).map(f => <li><Check /> {f}</li>)}
</ul>
Enter fullscreen mode Exit fullscreen mode

Three claims from the license row, three from the product. Six bullets per card. The diff between Single and Team becomes immediately readable: one developer, one project vs. up to N developers, N projects. That's the comparison the buyer is making.

Fix 3: refund/ownership line under the CTA

<CardFooter>
  <BuyButton ... />
  <p className="text-xs text-muted-foreground mt-3">
    7-day refund · lifetime updates · own the code
  </p>
</CardFooter>
Enter fullscreen mode Exit fullscreen mode

The last objection happens at the click. The answer has to live there.

Side-by-side

Version Change Conversion
V1 Identical cards, feature matrix, .toFixed(2) ~1.1%
V2 "Most Popular" badge on multiple, price-sorted ~1.9%
V3 Drop .00, 6 bullets, refund line under CTA ~3.4%

The actual lesson for devs

Most of what helped wasn't pricing strategy. It was deleting fussiness:

  • Layout fussiness → anchor a tier
  • Content fussiness → cut to 6 bullets
  • Formatting fussiness → drop trailing zeros

Three rewrites, three layers peeled. Ship them in order, not as one A/B test. Watch each for two weeks.

What to copy tonight

// 1. Anchor the middle tier
const isHighlighted = license.license_type === "multiple";

// 2. Smart price rendering
const price = Number(license.price_usd);
const display = price % 1 === 0 ? price : price.toFixed(2);

// 3. Refund line lives by the CTA, not the page footer
<BuyButton />
<p className="text-xs">7-day refund · lifetime updates</p>
Enter fullscreen mode Exit fullscreen mode

That's the diff. The component is in components/pricing-cards.tsx. The rest of the engineering — Supabase rows, Stripe Checkout, Edge Function license grants — didn't change once across all three rewrites.

For more on the React Native + Expo stack behind this, the Expo docs on EAS Build and Stripe's pricing experiments guide are the two external reads worth the time.


What's the smallest pricing-page change that moved a real number for you? Drop the diff in the comments — I'm collecting examples from indie devs and small SaaS teams.

Top comments (0)