Pricing
A polished marketing pricing landing page — hero, logo marquee, plan cards, sticky comparison table, impact band, testimonial, FAQ, and CTA.
Overview
The Pricing page is a full marketing landing page rendered inside the app. It reads like a real SaaS pricing page, not a dashboard screen: it is full-bleed, forces header-only presentation (no app sidebar), and hides the chat widgets — so the page owns the full viewport width. The editorial flow is:
hero (rotating headline + ambient canvas) → customer-logo marquee → plans (monthly/annual toggle, 4 tiers) → sticky comparison table → impact-metrics band → testimonial → FAQ → CTA, over a page-wide dot-grid texture, with animated wave dividers between full-width bands.
It is built from focused section components in src/components/pricing/, driven by data tables in
src/data/pricing.ts. All copy is i18n (the pricing namespace, plus nav for the tab title);
every color is a semantic token (skin- and dark-safe); and all page text is pinned to Plus
Jakarta Sans via a single inline --font-* override on the page root. It leans on the free
motion/react FX components (DotGrid, WaveDivider, HeroCanvas, RotatingWord, Reveal,
Parallax, Stagger, AnimatedNumber, FadeIn) — see Animation & Effects.
Architecture & files
| File | Responsibility |
|---|---|
src/pages/apps/PricingPage.tsx | Composes the sections in editorial order over a page-wide DotGrid; pins all text to Plus Jakarta Sans; sets the tab title. |
src/data/pricing.ts | Single source of truth: TIERS, COMPARE, FAQS, LOGOS, HERO_STATS, IMPACT_STATS, the BillingCycle type, ANNUAL_DISCOUNT, and JAKARTA_FONT_STACK. Stores i18n keys; prices/numbers stay literal. |
src/components/pricing/PricingHero.tsx | Hero section: badge, rotating headline word, subtitle, CTAs, trust-stat strip over HeroCanvas, wave divider. |
src/components/pricing/TrustedBy.tsx | Customer-logo band (wraps LogoMarquee). |
src/components/pricing/PricingPlans.tsx | Plans section: heading, the monthly/annual billing Tabs toggle (owns the cycle state), the "Save 20%" incentive, and the staggered PricingCard grid. |
src/components/pricing/PricingCompare.tsx | Comparison section (wraps CompareTable). |
src/components/pricing/ImpactBand.tsx | "Why teams switch" impact-metrics band (wraps StatGrid variant="impact"). |
src/components/pricing/PricingTestimonial.tsx | Testimonial / social-proof block. |
src/components/pricing/PricingFaq.tsx | FAQ accordion (from FAQS). |
src/components/pricing/PricingCta.tsx | Closing call-to-action band. |
src/components/pricing/SectionHead.tsx | Reusable editorial section header (eyebrow + display title + subtitle, center/left). |
src/components/pricing/PricingCard.tsx | One tier card: free / paid / contact-sales, annual-savings anchoring, and the elevated "Most popular" treatment. |
src/components/pricing/CompareTable.tsx | The feature-comparison table with sticky tier headers on scroll and a repeated CTA footer row. |
src/components/pricing/StatGrid.tsx | Count-up stat row/grid shared by the hero strip (hero) and impact band (impact). |
src/components/pricing/LogoMarquee.tsx | Renders LOGOS as real logo lockups (lucide mark + wordmark). |
src/components/pricing/index.ts | Barrel re-exporting the reusable components + page sections. |
Data model
export type BillingCycle = 'monthly' | 'annual'
export interface PricingTier {
id: string
nameKey: string
taglineKey: string
monthly: number // per-month price billed monthly
annual: number // per-month price billed annually (already discounted)
ctaKey: string
ctaVariant: 'primary' | 'outline'
featureKeys: string[] // i18n keys for the bullet list
popular?: boolean // elevated "Most popular" treatment
contactSales?: boolean // show "Contact sales" instead of a price
isFree?: boolean // show "$0 / Free forever"
}The seed ships four tiers — Free, Pro (popular), Business, and Enterprise
(contactSales). Annual figures are the discounted per-month equivalent so the card shows one "/mo"
value for either cycle; ANNUAL_DISCOUNT drives the "Save 20%" copy (note: the per-tier annual
prices are authored independently, so keep them in sync if you change the discount).
COMPARE is an array of CompareGroups (Core / Collaboration / Support), each with CompareRows
whose values are indexed to TIERS order — a boolean renders a check/dash, a string renders the
value (a cell* string is itself an i18n key). FAQS, LOGOS, HERO_STATS, and IMPACT_STATS are
similarly plain data arrays.
Presentation (route flags)
// src/routes.tsx
{path: '/apps/pricing', element: <PricingPage />, fullBleed: true, headerOnly: true, hideWidgets: true},These are per-route presentation flags (see Layout System) — they force
the marketing-page look without mutating the user's saved layout config: fullBleed drops the <main>
padding, headerOnly hides the app sidebar, and hideWidgets suppresses the chat rail/bubble.
Usage
Navigate to /apps/pricing. The page is self-contained:
import {PricingPage} from '@/pages/apps/PricingPage'
;<PricingPage />The reusable pieces can be dropped into any page via the barrel:
import {SectionHead, PricingCard, CompareTable, StatGrid} from '@/components/pricing'
import {TIERS, HERO_STATS} from '@/data/pricing'API / Props
The sections (PricingHero, TrustedBy, PricingPlans, …) take no props — they read from
src/data/pricing.ts and translate their own keys. The reusable building blocks are parameterized:
SectionHead
| Prop | Type | Description |
|---|---|---|
eyebrow | string | Small uppercase label with a leading rule. |
title | string | Display heading. |
subtitle? | string | Optional supporting line. |
align? | 'center' | 'left' | Default 'center'. |
className? | string | Extra classes. |
PricingCard
| Prop | Type | Description |
|---|---|---|
tier | PricingTier | The tier data. |
cycle | BillingCycle | 'monthly' or 'annual' — selects which price to show (with strikethrough anchor + animated count-up on change). |
Handles the free / paid / contact-sales branches, the "Most popular" strip + ring + lift, the tier
icon (mapped by tier.id), and the feature checklist.
StatGrid
| Prop | Type | Description |
|---|---|---|
stats | Stat[] | {value, prefix?, suffix?, decimals?, labelKey} entries. |
variant | 'hero' | 'impact' | hero = compact inline row (foreground); impact = large emphasised grid (primary). |
CompareTable / LogoMarquee
Take no props — they render COMPARE + TIERS and LOGOS respectively. CompareTable sticks the
tier-name header row under the app header on scroll (offset by --page-header-sticky-top) and repeats
the plan CTAs in a footer row.
Configuration & customization
Edit / add / remove a plan
Everything is data-driven from TIERS in src/data/pricing.ts:
export const TIERS: PricingTier[] = [
{
id: 'pro',
nameKey: 'proName',
taglineKey: 'proTagline',
monthly: 29,
annual: 23, // discounted per-month equivalent
ctaKey: 'ctaStart',
ctaVariant: 'primary',
popular: true,
featureKeys: ['proF1', 'proF2', 'proF3', 'proF4', 'proF5'],
},
// add / remove tiers here…
]When you add a tier: (1) add the entry, (2) add its nameKey / taglineKey / featureKeys / ctaKey
labels to src/locales/<lng>/pricing.json, (3) add a value to every CompareRow.values array in
COMPARE (values are indexed to TIERS order), and (4) optionally map a tier icon by id in
TIER_ICONS inside PricingCard.tsx. The plan grid, comparison table columns, and the comparison
footer CTAs all derive from TIERS.
Info
The grid is laid out xl:grid-cols-4; four tiers is the intended count. Adding more will wrap —
adjust the grid classes in PricingPlans.tsx if you need a different column count.
Comparison rows
Add a CompareRow under the relevant CompareGroup. Use true/false for check/dash, a plain string
for a literal cell, or a cell* string (itself an i18n key) for a translated cell. hintKey adds a
tooltip next to the feature name.
FAQ, logos, stats
Edit the FAQS, LOGOS, HERO_STATS, and IMPACT_STATS arrays in src/data/pricing.ts. LOGOS
icon values are lucide icon keys resolved to components in LogoMarquee.tsx.
Fonts & colors
All page text is pinned to Plus Jakarta Sans via a single inline --font-* override on the page root
(jakartaVars in PricingPage.tsx, using JAKARTA_FONT_STACK) — this beats the skin / html[lang]
font rules for the page subtree. Colors are semantic tokens only, so the page adapts to every skin and
dark mode without edits.
Examples
Reuse a pricing card grid elsewhere
import {useState} from 'react'
import {PricingCard} from '@/components/pricing'
import {TIERS, type BillingCycle} from '@/data/pricing'
function MiniPricing() {
const [cycle, setCycle] = useState<BillingCycle>('monthly')
return (
<div className="grid gap-5 sm:grid-cols-2 xl:grid-cols-4">
{TIERS.map((tier) => (
<PricingCard key={tier.id} tier={tier} cycle={cycle} />
))}
</div>
)
}An editorial section header + impact stats
import {SectionHead, StatGrid} from '@/components/pricing'
import {IMPACT_STATS} from '@/data/pricing'
;<>
<SectionHead eyebrow="Impact" title="Why teams switch" />
<StatGrid stats={IMPACT_STATS} variant="impact" />
</>Best practices
- Keep
TIERSandCOMPAREin sync. Comparisonvaluesare positional — one entry per tier, inTIERSorder. Adding a tier without extending every row shifts the columns. - Store copy as keys. Names, taglines, features, FAQs, CTA labels are i18n keys; only prices and raw numbers stay literal.
- Tokens for color,
motion.tsfor motion. Never hardcode hex; reuse the FX wrappers rather than rawmotion.*. - Prices in one place. The annual price is authored per tier (not derived) — if you change
ANNUAL_DISCOUNT, update eachannualvalue and the "Save 20%" copy together. - Don't reach for Motion+. The hero canvas, rotating word, dot-grid, and wave dividers are all
free-core reimplementations — keep it that way (see the no-Motion+ rule in
CLAUDE.md).
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| Comparison columns look shifted | A CompareRow.values array has the wrong length — it must have exactly one value per tier, in TIERS order. |
| Sticky tier headers hide behind the app header | CompareTable offsets the sticky row by --page-header-sticky-top; keep that offset if you restyle it. |
| Rows bleed through the "Most popular" sticky cell | The popular header uses an opaque color-mix tint (not a translucent /[0.06]) precisely so scrolling rows don't show through — don't switch it to an alpha class. |
| The sidebar / chat widgets appear on the page | The marketing look depends on the route flags headerOnly + hideWidgets in src/routes.tsx; keep them on the route entry. |
| Fonts don't match the rest of the app | Intentional — the page pins Plus Jakarta Sans via jakartaVars. Remove that override to inherit the skin font. |
| Annual price wrong / discount mismatched | annual is authored independently of ANNUAL_DISCOUNT; edit the per-tier value. |
FAQ
Does the checkout / CTA do anything? No — the CTAs are presentational (marketing demo). Wire them
to your billing flow or the api client as needed.
Is billing state global? No — the monthly/annual cycle lives only in PricingPlans (it's used
just there). It is not persisted.
Can I use the sections outside this page? Yes — the reusable pieces (SectionHead, PricingCard,
CompareTable, StatGrid, LogoMarquee) are exported from the barrel and take plain props/data.
Why is it under /apps? It's demoed as an in-app landing page (a common ThemeForest showcase),
using the per-route presentation flags to shed the dashboard chrome.
Notes for designers & content editors
- All copy — plan names, taglines, feature bullets, comparison labels, FAQs, testimonial, CTA — is
i18n keys in
src/locales/<lng>/pricing.json(the tab title comes fromnav). Prices and numbers are literal insrc/data/pricing.ts. - Colors are semantic tokens; the whole page re-skins and dark-modes automatically. The "Most
popular" accent follows
--primary. - Motion is reduced-motion safe — the canvas, rotating word, parallax, and count-ups all respect the global reduced-motion setting.
- Logos are icon + wordmark lockups (not images), so they inherit token colors and stay crisp.
- Fonts are pinned to Plus Jakarta Sans for a consistent marketing voice, independent of the active skin/language font.
Related
Layout System
Full-bleed, headerOnly, and hideWidgets route flags.
Animation & Effects
DotGrid, WaveDivider, HeroCanvas, RotatingWord, and more.
Components
The Tabs billing toggle, FAQ Accordion, Card, Badge, Button.
Design Tokens & Dark Mode
The semantic tokens every section uses.
Architecture & Routing
The route registry and per-route presentation.
Was this page helpful?
