Forem

huangyongshan46-a11y
huangyongshan46-a11y

Posted on

SaaS Landing Page That Converts: Next.js + Tailwind CSS (With Full Code)

Most SaaS landing pages fail before a user reads a single word. Bad hierarchy, weak CTAs, bloated component libraries — and the product never gets a fair shot.

➡️ LaunchKit homepage — full details, pricing, and demo.

This guide shows you exactly how to build a landing page that converts, using Next.js 14+ (App Router) and Tailwind CSS v4. No UI kit dependencies. Just clean, fast, opinionated code.

We'll cover:

  • Hero section with a strong value proposition
  • Feature grid that sells benefits, not features
  • Pricing section with a recommended plan
  • Sticky CTA that follows the user

Let's ship it.


Project Setup

npx create-next-app@latest my-saas --typescript --tailwind --app
cd my-saas
Enter fullscreen mode Exit fullscreen mode

With Tailwind CSS v4, configuration lives in your CSS file — no tailwind.config.js needed:

/* app/globals.css */
@import "tailwindcss";

@theme {
  --color-brand: #6366f1;
  --color-brand-dark: #4f46e5;
  --font-sans: "Inter", sans-serif;
}
Enter fullscreen mode Exit fullscreen mode

Simple. No plugin juggling. Let's build.


1. Hero Section

Your hero has one job: make the visitor say "this is for me." Headline, sub-headline, and a single CTA. That's it.

// components/Hero.tsx
export function Hero() {
  return (
    <section className="relative isolate overflow-hidden bg-white px-6 pt-24 pb-20 sm:pt-32 lg:px-8">
      {/* Subtle gradient background */}
      <div
        aria-hidden="true"
        className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80"
      >
        <div
          className="relative left-[calc(50%-11rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-indigo-200 to-violet-400 opacity-30 sm:left-[calc(50%-30rem)] sm:w-[72.1875rem]"
          style={{
            clipPath:
              "polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.3%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)",
          }}
        />
      </div>

      <div className="mx-auto max-w-2xl text-center">
        {/* Social proof badge */}
        <div className="mb-8 flex justify-center">
          <span className="rounded-full bg-indigo-50 px-4 py-1.5 text-sm font-medium text-indigo-700 ring-1 ring-inset ring-indigo-700/10">
            Trusted by 500+ founders
          </span>
        </div>

        <h1 className="text-5xl font-bold tracking-tight text-gray-900 sm:text-7xl">
          Ship your SaaS{" "}
          <span className="text-indigo-600">in days, not months</span>
        </h1>

        <p className="mt-6 text-lg leading-8 text-gray-600">
          Everything you need to launch: auth, payments, email, and a
          conversion-optimized landing page. Stop rebuilding the same boilerplate
          and start building what matters.
        </p>

        <div className="mt-10 flex items-center justify-center gap-x-6">
          <a
            href="#pricing"
            className="rounded-lg bg-indigo-600 px-6 py-3 text-base font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 transition-colors"
          >
            Get started — $49
          </a>
          <a
            href="#features"
            className="text-base font-semibold leading-7 text-gray-900 hover:text-indigo-600 transition-colors"
          >
            See what's included <span aria-hidden="true"></span>
          </a>
        </div>
      </div>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

What makes this work:

  • Social proof badge appears before the headline — primes trust instantly
  • Headline speaks to the outcome (ship faster), not the tool
  • Two CTAs: one primary (paid), one secondary (learn more) — never leave users with nowhere to go
  • The gradient blob is decorative but signals "modern SaaS" visually

2. Features Grid

Don't list features. Sell transformations. Users don't care that you have "Stripe integration" — they care that they don't have to spend a weekend wiring it up themselves.

// components/Features.tsx
const features = [
  {
    icon: "",
    title: "Launch in hours",
    description:
      "Pre-wired auth, payments, and email. Clone, configure, deploy. Your competitors are still setting up their dev environment.",
  },
  {
    icon: "🔒",
    title: "Auth that just works",
    description:
      "Google OAuth, magic links, and session management — all done. No more reading docs at 2am wondering why cookies aren't persisting.",
  },
  {
    icon: "💳",
    title: "Stripe, without the headache",
    description:
      "Subscriptions, one-time payments, webhooks, and a billing portal. Tested, production-ready, and actually maintainable.",
  },
  {
    icon: "📧",
    title: "Transactional email",
    description:
      "Welcome emails, password resets, upgrade confirmations — templated and ready. Resend integration included.",
  },
  {
    icon: "🚀",
    title: "Deploy anywhere",
    description:
      "Optimized for Vercel out of the box. Docker config included for self-hosting. Your infra, your rules.",
  },
  {
    icon: "🎨",
    title: "Tailwind CSS v4",
    description:
      "Built on the latest Tailwind with a design system you can actually customize. Not a theme you have to fight.",
  },
];

export function Features() {
  return (
    <section id="features" className="bg-gray-50 py-24 sm:py-32">
      <div className="mx-auto max-w-7xl px-6 lg:px-8">
        <div className="mx-auto max-w-2xl text-center">
          <h2 className="text-base font-semibold leading-7 text-indigo-600">
            Everything included
          </h2>
          <p className="mt-2 text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
            Stop rebuilding. Start shipping.
          </p>
          <p className="mt-6 text-lg leading-8 text-gray-600">
            Every feature your SaaS needs from day one. No piecing together
            tutorials. No debugging someone else's half-baked boilerplate.
          </p>
        </div>

        <div className="mx-auto mt-16 max-w-7xl sm:mt-20 lg:mt-24">
          <dl className="grid max-w-xl grid-cols-1 gap-x-8 gap-y-10 lg:max-w-none lg:grid-cols-3 lg:gap-y-16">
            {features.map((feature) => (
              <div key={feature.title} className="relative pl-4">
                <dt className="flex items-center gap-3 text-base font-semibold leading-7 text-gray-900">
                  <span className="text-2xl">{feature.icon}</span>
                  {feature.title}
                </dt>
                <dd className="mt-2 text-base leading-7 text-gray-600">
                  {feature.description}
                </dd>
              </div>
            ))}
          </dl>
        </div>
      </div>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

The pattern: Icon → Outcome headline → One-sentence consequence of not having this. Every description answers "so what?" That's what sells.


3. Pricing Section

Pricing pages kill conversions more than anything else. The fix: one recommended plan, clearly marked, with a direct purchase link. Don't make people think.

// components/Pricing.tsx
const plans = [
  {
    name: "Starter",
    price: "$0",
    description: "For tinkerers and side projects.",
    features: ["Next.js boilerplate", "Basic auth", "Community support"],
    cta: "Start free",
    href: "#",
    featured: false,
  },
  {
    name: "LaunchKit",
    price: "$49",
    description: "One-time payment. Everything you need to ship.",
    features: [
      "Full Next.js + Tailwind v4 stack",
      "Auth (OAuth + magic links)",
      "Stripe subscriptions & billing",
      "Transactional email (Resend)",
      "Conversion-optimized landing page",
      "Deploy configs (Vercel + Docker)",
      "Lifetime updates",
    ],
    cta: "Buy now — $49",
    href: "https://yongshan5.gumroad.com/l/xckqag",
    featured: true,
  },
];

export function Pricing() {
  return (
    <section id="pricing" className="bg-white py-24 sm:py-32">
      <div className="mx-auto max-w-7xl px-6 lg:px-8">
        <div className="mx-auto max-w-2xl text-center">
          <h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
            Simple, honest pricing
          </h2>
          <p className="mt-6 text-lg leading-8 text-gray-600">
            No subscriptions. No seat limits. Pay once, ship forever.
          </p>
        </div>

        <div className="mx-auto mt-16 grid max-w-5xl grid-cols-1 gap-8 lg:grid-cols-2">
          {plans.map((plan) => (
            <div
              key={plan.name}
              className={`rounded-3xl p-8 ring-1 ${
                plan.featured
                  ? "bg-indigo-600 ring-indigo-600"
                  : "bg-white ring-gray-200"
              }`}
            >
              {plan.featured && (
                <div className="mb-4">
                  <span className="rounded-full bg-white/20 px-3 py-1 text-sm font-medium text-white">
                    Most popular
                  </span>
                </div>
              )}

              <h3
                className={`text-lg font-semibold leading-8 ${
                  plan.featured ? "text-white" : "text-gray-900"
                }`}
              >
                {plan.name}
              </h3>

              <p
                className={`mt-4 text-5xl font-bold tracking-tight ${
                  plan.featured ? "text-white" : "text-gray-900"
                }`}
              >
                {plan.price}
              </p>

              <p
                className={`mt-2 text-sm ${
                  plan.featured ? "text-indigo-200" : "text-gray-500"
                }`}
              >
                {plan.description}
              </p>

              <ul
                className={`mt-8 space-y-3 text-sm leading-6 ${
                  plan.featured ? "text-indigo-100" : "text-gray-600"
                }`}
              >
                {plan.features.map((feature) => (
                  <li key={feature} className="flex gap-x-3">
                    <span className={plan.featured ? "text-white" : "text-indigo-600"}></span>
                    {feature}
                  </li>
                ))}
              </ul>

              <a
                href={plan.href}
                className={`mt-8 block rounded-lg px-4 py-3 text-center text-sm font-semibold transition-colors ${
                  plan.featured
                    ? "bg-white text-indigo-600 hover:bg-indigo-50"
                    : "bg-indigo-600 text-white hover:bg-indigo-500"
                }`}
              >
                {plan.cta}
              </a>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why this works: The featured plan uses inverted colors (dark background, light text) — it draws the eye immediately. The free tier exists only to anchor the paid tier's value. "Pay once, ship forever" eliminates subscription anxiety.


4. Sticky CTA Bar

Once users scroll past your hero, a sticky bottom bar keeps the conversion path visible. Subtle, not annoying.

// components/StickyBar.tsx
"use client";
import { useState, useEffect } from "react";

export function StickyBar() {
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const onScroll = () => setVisible(window.scrollY > 400);
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  return (
    <div
      className={`fixed bottom-0 inset-x-0 z-50 transition-transform duration-300 ${
        visible ? "translate-y-0" : "translate-y-full"
      }`}
    >
      <div className="bg-gray-900 px-6 py-4">
        <div className="mx-auto flex max-w-7xl items-center justify-between gap-4">
          <p className="text-sm font-medium text-white">
            🚀 Ship your SaaS faster — everything included, one-time payment
          </p>
          <a
            href="#pricing"
            className="shrink-0 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500 transition-colors"
          >
            Get LaunchKit — $49
          </a>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Appears after 400px of scroll. Disappears on the way back up. No jank, no layout shift.


Wiring It All Together

// app/page.tsx
import { Hero } from "@/components/Hero";
import { Features } from "@/components/Features";
import { Pricing } from "@/components/Pricing";
import { StickyBar } from "@/components/StickyBar";

export default function Home() {
  return (
    <main>
      <Hero />
      <Features />
      <Pricing />
      <StickyBar />
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Clean. No mystery. Each section is independently testable and replaceable.


What Actually Moves the Needle

Here's the opinionated part. After building and studying dozens of SaaS landing pages, the patterns that actually convert:

  1. Lead with outcomes, not features. "Ship in days" > "includes auth"
  2. Social proof early. Badges and numbers before the fold, not after
  3. One primary CTA. Every additional option reduces conversion
  4. Price on the page. Hiding pricing filters out nothing useful and creates friction
  5. Fast. Tailwind CSS v4 with Next.js App Router and no heavy UI kit = 95+ Lighthouse score by default. Speed is conversion.

The landing page for LaunchKit follows all of these — you can browse the full source to see how sections, animations, and mobile layout are handled in a production-ready codebase.


Final Thoughts

A landing page isn't a brochure — it's a sales tool. Every section should serve a purpose: establish trust, explain value, and remove friction from the purchase decision.

The code above isn't starter template filler. It's the actual structure that performs. Use the patterns, iterate on copy, and A/B test your headline — that'll move the needle more than any animation library.


If you don't want to build all this from scratch, LaunchKit ships with a complete conversion-optimized landing page. $49 on Gumroad — or browse the preview on GitHub.

Top comments (0)