Skip to content

SaaS landing page

Eleven blocks in the order a subscription product needs: claim, proof, mechanism, evidence, price, objections, action.

LandingintermediateFeaturedsaaslandingconversionpricing

Live preview

Open the full page
full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

This exact file renders the preview above.

import SaasNavbar from '@/components/blocks/navigation/saas'
import SaasHeader from '@/components/blocks/headers/saas'
import WordmarkLogos from '@/components/blocks/sections/logos/wordmarks'
import AlternatingFeatures from '@/components/blocks/sections/features/alternating'
import FeatureIconGrid from '@/components/blocks/sections/features/icon-grid'
import StatsWithContext from '@/components/blocks/sections/stats/with-context'
import ThreeColumnTestimonials from '@/components/blocks/sections/testimonials/three-column'
import ThreeTierPricing from '@/components/blocks/sections/pricing/three-tier'
import AccordionFAQ from '@/components/blocks/sections/faq/accordion'
import InvertedPanelCTA from '@/components/blocks/sections/cta/inverted-panel'
import SaasFooter from '@/components/blocks/footers/saas'

/**
 * SaaS landing page
 *
 * Eleven sections in the order that a subscription product's landing page
 * needs them: claim, proof, mechanism, evidence, price, objections, action.
 *
 * Every block is imported from the catalogue — nothing here is bespoke, which
 * is the whole demonstration. Composing a page is choosing an order, not
 * writing new markup.
 */
export default function SaasLandingPattern() {
  return (
    <>
      <SaasNavbar />
      <main>
        <SaasHeader />
        <WordmarkLogos />
        <AlternatingFeatures />
        <FeatureIconGrid />
        <StatsWithContext />
        <ThreeColumnTestimonials />
        <ThreeTierPricing />
        <AccordionFAQ />
        <InvertedPanelCTA />
      </main>
      <SaasFooter />
    </>
  )
}

components/patterns/saas-landing.tsx

import SaasNavbar from '@/components/blocks/navigation/saas'
import SaasHeader from '@/components/blocks/headers/saas'
import WordmarkLogos from '@/components/blocks/sections/logos/wordmarks'
import AlternatingFeatures from '@/components/blocks/sections/features/alternating'
import FeatureIconGrid from '@/components/blocks/sections/features/icon-grid'
import StatsWithContext from '@/components/blocks/sections/stats/with-context'
import ThreeColumnTestimonials from '@/components/blocks/sections/testimonials/three-column'
import ThreeTierPricing from '@/components/blocks/sections/pricing/three-tier'
import AccordionFAQ from '@/components/blocks/sections/faq/accordion'
import InvertedPanelCTA from '@/components/blocks/sections/cta/inverted-panel'
import SaasFooter from '@/components/blocks/footers/saas'

/**
 * SaaS landing page
 *
 * Eleven sections in the order that a subscription product's landing page
 * needs them: claim, proof, mechanism, evidence, price, objections, action.
 *
 * Every block is imported from the catalogue — nothing here is bespoke, which
 * is the whole demonstration. Composing a page is choosing an order, not
 * writing new markup.
 */
export default function SaasLandingPattern() {
  return (
    <>
      <SaasNavbar />
      <main>
        <SaasHeader />
        <WordmarkLogos />
        <AlternatingFeatures />
        <FeatureIconGrid />
        <StatsWithContext />
        <ThreeColumnTestimonials />
        <ThreeTierPricing />
        <AccordionFAQ />
        <InvertedPanelCTA />
      </main>
      <SaasFooter />
    </>
  )
}

components/blocks/headers/saas.tsx

import { ArrowRight, Check } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { ButtonLink } from '@/components/ui/button'
import { Container } from '@/components/ui/layout'

/**
 * SaaS header
 *
 * Announcement pill, headline, subhead, paired actions and three
 * objection-handling proof points. The proof row is the part that earns its
 * place — "no credit card" answers the question the primary button raises.
 */
const proof = ['No credit card required', '14-day trial', 'Cancel anytime']

export default function SaasHeader() {
  return (
    <header className="border-b border-line bg-canvas py-section">
      <Container className="text-center">
        <Badge tone="accent" appearance="soft" className="mx-auto">
          Foundry 1.0 is available
        </Badge>

        <h1 className="display-type mx-auto mt-6 max-w-3xl text-3xl leading-tight font-semibold text-ink-strong sm:text-4xl lg:text-5xl">
          Ship your interface in an afternoon, not a quarter
        </h1>

        <p className="mx-auto mt-5 max-w-2xl text-md text-ink-muted sm:text-lg">
          Every primitive, section and page pattern your product needs — accessible, themeable and
          server-rendered by default.
        </p>

        <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
          <ButtonLink
            href="/starters/saas/preview/register"
            size="lg"
            trailingIcon={<ArrowRight className="size-4" />}
          >
            Start free trial
          </ButtonLink>
          <ButtonLink href="/patterns" size="lg" variant="outline">
            See it assembled
          </ButtonLink>
        </div>

        <ul className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-2">
          {proof.map((item) => (
            <li key={item} className="flex items-center gap-1.5 text-sm text-ink-muted">
              <Check className="size-4 shrink-0 text-success" aria-hidden="true" />
              {item}
            </li>
          ))}
        </ul>
      </Container>
    </header>
  )
}

components/blocks/sections/pricing/three-tier.tsx

import { Check } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { ButtonLink } from '@/components/ui/button'
import { Container } from '@/components/ui/layout'
import { cn } from '@/lib/cn'
import { pricingTiers } from '@/content/demo'

/**
 * Three-tier pricing
 *
 * The recommended tier is raised rather than merely tinted: a border, a badge
 * and elevation, so the recommendation survives greyscale and a colour-blind
 * reader. Cards are equal height with the action pinned to the bottom, so the
 * three buttons align regardless of feature-list length.
 */
export default function ThreeTierPricing() {
  return (
    <section className="border-b border-line bg-canvas py-section">
      <Container>
        <div className="mx-auto max-w-2xl text-center">
          <h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
            Pricing that fits how teams actually adopt this.
          </h2>
          <p className="mx-auto mt-3 max-w-lg text-md text-ink-muted">
            Fictional pricing for a fictional product — the layout is the part worth copying.
          </p>
        </div>

        <ul className="mt-12 grid gap-4 lg:grid-cols-3">
          {pricingTiers.map((tier) => (
            <li key={tier.name}>
              <div
                className={cn(
                  'flex h-full flex-col rounded-xl border bg-surface p-6 sm:p-8',
                  tier.featured ? 'border-accent shadow-md lg:-my-4 lg:py-10' : 'border-line',
                )}
              >
                <div className="flex items-center justify-between gap-3">
                  <h3 className="display-type text-lg font-semibold text-ink-strong">
                    {tier.name}
                  </h3>
                  {tier.featured ? (
                    <Badge tone="accent" appearance="solid">
                      Recommended
                    </Badge>
                  ) : null}
                </div>

                <p className="mt-2 text-sm text-ink-muted">{tier.description}</p>

                <p className="mt-6 flex items-baseline gap-1.5">
                  <span className="display-type text-4xl font-semibold text-ink-strong tabular-nums">
                    {tier.price}
                  </span>
                  <span className="text-sm text-ink-muted">{tier.cadence}</span>
                </p>

                <ul className="mt-6 flex flex-1 flex-col gap-2.5">
                  {tier.features.map((feature) => (
                    <li key={feature} className="flex items-start gap-2.5 text-sm text-ink">
                      <Check className="mt-0.5 size-4 shrink-0 text-success" aria-hidden="true" />
                      {feature}
                    </li>
                  ))}
                </ul>

                <ButtonLink
                  href={tier.featured ? '/forms/register' : '/forms/sales-inquiry'}
                  variant={tier.featured ? 'primary' : 'outline'}
                  block
                  className="mt-8"
                >
                  {tier.cta}
                </ButtonLink>

                {tier.note ? (
                  <p className="mt-3 text-center text-xs text-ink-subtle">{tier.note}</p>
                ) : null}
              </div>
            </li>
          ))}
        </ul>
      </Container>
    </section>
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

The reference composition for a subscription product. Read the source: it is nine imports and a fragment, which is the entire argument for a section library.

  • Proof comes immediately after the claim — a logo row directly under the hero is doing more work than a second feature section.
  • Pricing sits before the FAQ, because most FAQ traffic arrives from the pricing section.
  • Swap any block for a sibling from the same category; the page will still hold together.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • SaaS navbar with product dropdown
  • Centred hero with proof row
  • Alternating and grid feature sections
  • Three-tier pricing and FAQ

Accessibility

One h1
Only the hero uses h1; every other section leads with h2, so the outline is flat and predictable.
Landmarks
Navigation, main and contentinfo are all present exactly once.
Client islands
Only the navbar dropdown and the FAQ disclosure hydrate; the rest is static markup.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.