EmpireUI
Get Pro
← Blog9 min read#tailwind#saas#marketing

SaaS Marketing Page in Tailwind: Hero, Features, Pricing, FAQ

Build a complete SaaS marketing page with Tailwind CSS — hero, features grid, pricing table, and FAQ accordion. Real code, real patterns, no fluff.

Tailwind CSS code on dark background for SaaS landing page

Why Tailwind Is the Right Call for SaaS Marketing Pages

Let's skip the debate. If you're building a SaaS marketing site in 2026 and you're not using Tailwind, you're probably managing a 4,000-line stylesheet that nobody wants to touch. Tailwind gives you the utility-first primitives that map almost 1:1 to the design decisions you're already making in Figma — spacing, color, type scale, responsive breakpoints.

The setup time matters too. With Tailwind v4 (released early 2026), you drop a single @import "tailwindcss" in your CSS, configure via @theme, and you're writing components in minutes. No config file, no purge setup, no PostCSS archaeology. That said, a marketing page has very specific structure requirements — hero, feature section, pricing, FAQ — and the order you build them in actually matters for reuse.

Honestly, a lot of SaaS landing pages are just the same six sections re-skinned. Hero with a headline and CTA. Three-column feature grid. A pricing table. A FAQ accordion. Sometimes testimonials. You've seen it a hundred times. That repetition is actually good news — it means there's a proven pattern, and Tailwind's utilities let you wire it together fast without getting locked into a component library you'll regret in six months.

Worth noting: this entire page structure pairs really well with Empire UI components if you want pre-built animated variants. But in this article we're building raw — so you own every pixel and understand what's underneath.

Hero Section: Headline, Sub-copy, CTA, and Glow

The hero section is where you lose people or hook them. You've got maybe 3 seconds. So the structure is tight: big headline, one line of sub-copy, two buttons (primary + secondary), and optionally a background effect. Here's a clean Tailwind starting point:

export function HeroSection() {
  return (
    <section className="relative min-h-screen flex flex-col items-center justify-center text-center px-6 py-24 bg-gray-950 overflow-hidden">
      {/* Glow blob */}
      <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
        <div className="w-[600px] h-[400px] bg-violet-600/20 blur-[120px] rounded-full" />
      </div>

      <div className="relative z-10 max-w-3xl">
        <span className="inline-flex items-center gap-2 rounded-full border border-violet-500/30 bg-violet-500/10 px-4 py-1.5 text-sm text-violet-300 mb-6">
          Now in public beta
        </span>

        <h1 className="text-5xl md:text-7xl font-extrabold tracking-tight text-white leading-[1.05]">
          Ship faster.<br />
          <span className="text-transparent bg-clip-text bg-gradient-to-r from-violet-400 to-cyan-400">
            Break nothing.
          </span>
        </h1>

        <p className="mt-6 text-lg text-gray-400 max-w-xl mx-auto">
          The deployment platform for teams who can't afford downtime. Instant rollbacks,
          zero-config previews, and real-time alerts baked in.
        </p>

        <div className="mt-10 flex flex-wrap items-center justify-center gap-4">
          <a
            href="/signup"
            className="rounded-full bg-violet-600 hover:bg-violet-500 text-white font-semibold px-8 py-3.5 text-sm transition-colors"
          >
            Start for free
          </a>
          <a
            href="/demo"
            className="rounded-full border border-white/15 hover:border-white/30 text-white/80 hover:text-white font-medium px-8 py-3.5 text-sm transition-colors"
          >
            Watch demo →
          </a>
        </div>
      </div>
    </section>
  );
}

The blur-[120px] glow blob on bg-violet-600/20 is a classic trick — costs nothing in performance, reads immediately as "modern SaaS." Keep the blur value between 80px and 150px. Go above 200px and it starts disappearing on most monitors. Go below 60px and it looks like a bug.

One more thing — that badge above the headline (Now in public beta) has become a SaaS marketing staple for good reason. It gives first-time visitors something to anchor to. Whether you use it as a product status indicator or just a hook, it consistently improves scroll depth. Use border-violet-500/30 with bg-violet-500/10 for a glassy feel that doesn't fight the headline.

Quick aside: if you want a more dramatic hero background — aurora gradients, mesh, particles — the Empire UI aurora components drop straight into this layout. Swap the glow div for a <AuroraBackground> wrapper and you're done. Keep the z-index layering and the rest of the section code is unchanged.

Features Grid: Three Columns, Icon, and Prose

Feature grids are where a lot of SaaS pages go wrong. Too many features, too much text per card, icon sets that don't match. The rule that actually works: max six features, one short sentence per card, and a 24x24px icon. Anything longer than one sentence belongs in the docs.

const features = [
  { icon: "⚡", title: "One-click deploys", desc: "Push to main, we handle the rest. Average deploy time: 18 seconds." },
  { icon: "🔄", title: "Instant rollbacks", desc: "Something broke? Roll back to any previous build in under 5 seconds." },
  { icon: "🔍", title: "Preview environments", desc: "Every PR gets its own live URL, automatically." },
  { icon: "📊", title: "Real-time analytics", desc: "Request counts, error rates, and p99 latency — live, no plugins needed." },
  { icon: "🔐", title: "Secret management", desc: "Inject env vars per environment without ever committing credentials." },
  { icon: "🌐", title: "Edge caching", desc: "Static assets served from 47 edge locations globally." },
];

export function FeaturesSection() {
  return (
    <section className="bg-gray-950 py-24 px-6">
      <div className="max-w-6xl mx-auto">
        <h2 className="text-3xl md:text-4xl font-bold text-white text-center mb-4">
          Everything your team needs
        </h2>
        <p className="text-gray-400 text-center max-w-lg mx-auto mb-16">
          No plugins. No configuration marathons. Just the features you actually use.
        </p>

        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
          {features.map((f) => (
            <div
              key={f.title}
              className="rounded-2xl border border-white/8 bg-white/3 p-6 hover:border-violet-500/40 hover:bg-white/5 transition-colors"
            >
              <div className="text-2xl mb-4">{f.icon}</div>
              <h3 className="text-white font-semibold text-lg mb-2">{f.title}</h3>
              <p className="text-gray-400 text-sm leading-relaxed">{f.desc}</p>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

The bg-white/3 and border-white/8 values are doing a lot of heavy lifting here. It's a subtle glassmorphism effect without the full frosted-glass treatment — just enough to give the cards depth against a dark background. If you want to go harder on the glass effect, the glassmorphism generator will spit out the exact backdrop-filter and background values to swap in.

In practice, most developers over-engineer the hover states on feature cards. A simple hover:border-violet-500/40 is plenty. You don't need scale transforms, shadow pulses, or glow rings — that stuff draws attention to individual cards when you actually want people scanning the grid as a whole.

Look, if your feature list runs longer than six items, break it into two rows with a Show more toggle, or use a tab-based layout. Scrolling through twelve feature cards is a conversion killer. Nobody's doing that.

Pricing Table: Tiers, Toggle, and a Highlighted Card

Pricing sections are maybe the most studied UI pattern in SaaS. The structure that converts is almost always the same: three tiers (Free / Pro / Business), a monthly/annual toggle that shows a savings callout, and a visually differentiated "recommended" card — usually the middle one. You can see a production-ready version in the Empire UI pricing table, but here's the Tailwind foundation:

const plans = [
  {
    name: "Free",
    price: { monthly: 0, yearly: 0 },
    features: ["3 projects", "1 team member", "Community support"],
    cta: "Get started",
    highlight: false,
  },
  {
    name: "Pro",
    price: { monthly: 29, yearly: 19 },
    features: ["Unlimited projects", "5 team members", "Priority support", "Custom domains"],
    cta: "Start free trial",
    highlight: true,
  },
  {
    name: "Business",
    price: { monthly: 99, yearly: 79 },
    features: ["Everything in Pro", "Unlimited seats", "SLA guarantee", "SSO / SAML"],
    cta: "Contact sales",
    highlight: false,
  },
];

export function PricingSection({ annual }: { annual: boolean }) {
  return (
    <section className="bg-gray-950 py-24 px-6">
      <div className="max-w-5xl mx-auto">
        <h2 className="text-3xl md:text-4xl font-bold text-white text-center mb-16">
          Simple, transparent pricing
        </h2>

        <div className="grid grid-cols-1 md:grid-cols-3 gap-6 items-stretch">
          {plans.map((plan) => (
            <div
              key={plan.name}
              className={[
                "rounded-2xl p-8 flex flex-col border",
                plan.highlight
                  ? "border-violet-500 bg-violet-600/10 ring-1 ring-violet-500/40"
                  : "border-white/8 bg-white/3",
              ].join(" ")}
            >
              {plan.highlight && (
                <span className="inline-block self-start rounded-full bg-violet-600 text-white text-xs font-semibold px-3 py-1 mb-4">
                  Most popular
                </span>
              )}
              <h3 className="text-xl font-bold text-white">{plan.name}</h3>
              <div className="mt-4 mb-6">
                <span className="text-4xl font-extrabold text-white">
                  ${annual ? plan.price.yearly : plan.price.monthly}
                </span>
                <span className="text-gray-400 text-sm ml-1">/mo</span>
              </div>
              <ul className="space-y-3 mb-8 flex-1">
                {plan.features.map((f) => (
                  <li key={f} className="flex items-center gap-2 text-gray-300 text-sm">
                    <span className="text-violet-400">✓</span> {f}
                  </li>
                ))}
              </ul>
              <a
                href="/signup"
                className={[
                  "rounded-full text-center font-semibold py-3 text-sm transition-colors",
                  plan.highlight
                    ? "bg-violet-600 hover:bg-violet-500 text-white"
                    : "border border-white/15 hover:border-white/30 text-white/80 hover:text-white",
                ].join(" ")}
              >
                {plan.cta}
              </a>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

The annual toggle lives outside this component — you'd pass annual as a prop from a parent that holds a useState(false) boolean. Keeping the toggle logic separate means this section is testable in isolation and you can animate the price number change with Framer Motion if you want that satisfying flip effect.

That ring-1 ring-violet-500/40 on the highlighted card is a subtle trick — a second border that sits inside the main border gives the card a slightly glowing look without any actual glow CSS. It's one of those things that looks expensive and costs basically nothing.

Worth noting: the savings shown on annual pricing should always be displayed as a percentage badge near the toggle, not just in the price. "Save 34%" next to the yearly option consistently outperforms showing just the lower number. People respond to the percentage frame more than the absolute dollar value.

FAQ Accordion: Accessible, Animated, Copy-Paste Ready

The FAQ section is genuinely underrated. It's where you handle objections, reduce support tickets, and pick up long-tail SEO from people searching "[your product] vs X" or "does [your product] support Y." A static wall of text doesn't cut it — you want an accordion that's accessible, keyboard-navigable, and feels good to interact with.

"use client";
import { useState } from "react";

const faqs = [
  { q: "Do you offer a free trial?", a: "Yes — all paid plans start with a 14-day trial. No credit card required." },
  { q: "Can I cancel anytime?", a: "Absolutely. Cancel from your dashboard and you won't be charged again. We don't do annual lock-ins." },
  { q: "Is there a team plan?", a: "The Pro plan supports up to 5 seats. Business is unlimited. You can mix roles — admin, developer, viewer." },
  { q: "What happens to my data if I cancel?", a: "Your data stays accessible for 30 days after cancellation. Export everything in one click before you go." },
  { q: "Do you support SSO?", a: "SSO with SAML 2.0 is available on the Business plan. We support Okta, Azure AD, and Google Workspace out of the box." },
];

export function FAQSection() {
  const [open, setOpen] = useState<number | null>(null);

  return (
    <section className="bg-gray-950 py-24 px-6">
      <div className="max-w-2xl mx-auto">
        <h2 className="text-3xl md:text-4xl font-bold text-white text-center mb-12">
          Frequently asked questions
        </h2>

        <div className="divide-y divide-white/8">
          {faqs.map((faq, i) => (
            <div key={i} className="py-5">
              <button
                onClick={() => setOpen(open === i ? null : i)}
                className="flex w-full items-center justify-between text-left gap-4"
                aria-expanded={open === i}
              >
                <span className="text-white font-medium">{faq.q}</span>
                <span
                  className={`text-gray-400 transition-transform duration-200 ${
                    open === i ? "rotate-45" : ""
                  }`}
                >
                  +
                </span>
              </button>
              {open === i && (
                <p className="mt-3 text-gray-400 text-sm leading-relaxed pr-8">
                  {faq.a}
                </p>
              )}
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

The aria-expanded attribute is doing real accessibility work here — screen readers will announce whether each item is collapsed or expanded. Don't skip that. And the divide-y divide-white/8 trick replaces individual border-bottom declarations on each item, which is cleaner than adding a class to each FAQ row manually.

One more thing — the rotate-45 on the + to turn it into an × is an old trick, but it's the right one. It's simpler than swapping icons, the animation is handled purely by Tailwind's transition utilities, and it reads correctly to sighted users without any JS complexity.

If you want the animated open/close with height transition rather than a simple show/hide, wrap the answer in a div and use CSS grid from grid-rows-[0fr] to grid-rows-[1fr] on the container — it's the smoothest zero-JS-dependency height animation available in Tailwind today.

Putting It All Together: Page Layout and Navigation

Once you've got the four sections built, the top-level page is just stacking them. Use a sticky nav with a backdrop blur — it gives the nav depth as you scroll without a hard background color that fights your hero section's color story.

// app/page.tsx (Next.js App Router)
import { HeroSection } from "@/components/HeroSection";
import { FeaturesSection } from "@/components/FeaturesSection";
import { PricingSection } from "@/components/PricingSection";
import { FAQSection } from "@/components/FAQSection";

export default function MarketingPage() {
  return (
    <main className="bg-gray-950">
      <nav className="sticky top-0 z-50 border-b border-white/8 backdrop-blur-md bg-gray-950/80 px-6 py-4 flex items-center justify-between">
        <span className="font-bold text-white text-lg">YourProduct</span>
        <div className="flex items-center gap-6">
          <a href="#features" className="text-sm text-gray-400 hover:text-white transition-colors">Features</a>
          <a href="#pricing" className="text-sm text-gray-400 hover:text-white transition-colors">Pricing</a>
          <a href="/signup" className="rounded-full bg-violet-600 hover:bg-violet-500 text-white text-sm font-semibold px-5 py-2 transition-colors">
            Get started
          </a>
        </div>
      </nav>

      <HeroSection />
      <FeaturesSection />
      <PricingSection annual={false} />
      <FAQSection />
    </main>
  );
}

The backdrop-blur-md bg-gray-950/80 combination is the right call for dark SaaS nav bars. It gives you that frosted glass feel without going fully transparent — if you go bg-gray-950/50 you'll get color bleed from sections below that looks janky. 80% opacity is the sweet spot. Want a more opinionated glassmorphism nav? Browse the glassmorphism navbar designs for production-tested variants.

You'll want to anchor #features, #pricing, and #faq id attributes on each section's outer element for the nav links to work. Add scroll-mt-20 to each section so the sticky nav doesn't cover the heading when you jump to it — that's a small thing that trips people up every time.

For the full page performance picture: at production, this page should load in under 2 seconds on a 4G connection. Keep your hero image under 200kb (use next/image), defer any third-party scripts to strategy="lazyOnload", and use font-display: swap on your Google Font if you're using one. These aren't optional polish — they're the difference between a 90+ Lighthouse score and explaining to a stakeholder why the page feels slow.

Visual Polish: Gradients, Borders, and Small Details That Matter

The difference between a "built it in a weekend" marketing page and one that reads as premium usually comes down to four things: type scale, gradient use, border opacity, and spacing consistency. None of them are complicated, but you have to be intentional.

For type: your hero headline should be at least text-5xl on mobile and text-7xl on desktop. If you're using font-extrabold (900 weight), a tracking-tight of around -0.02em keeps it from feeling like a poster. Section headings live at text-3xl md:text-4xl — don't go bigger or they compete with the hero.

Gradients: the bg-gradient-to-r from-violet-400 to-cyan-400 on the hero headline is a reliable choice because violet-to-cyan reads as "modern tech" and works on both dark and light backgrounds. If you want to experiment with color combinations, the gradient generator gives you live previews with Tailwind class output — no guessing at oklch values. For cards and section dividers, use gradients sparingly: a from-violet-600/20 to-transparent border on a section divider is subtle and effective.

Border opacity is where most developers under-invest. border-white/8 is barely-there on dark backgrounds — right for cards you want to feel light. border-white/15 is medium — right for nav and modals. border-white/25 reads as a deliberate visual separation. Never use border-white (full opacity) on a dark theme — it's jarring. These aren't aesthetic preferences; they're established patterns from design systems at companies like Linear and Vercel that you can inspect in devtools right now.

One more thing — add an 8px (0.5rem) bottom padding to your last section and a proper <footer> below the FAQ. A page that cuts off sharply at the last section reads as unfinished. Even a minimal footer — logo, three nav links, copyright line — closes the page properly and gives you a natural spot for legal links you'll need eventually anyway.

FAQ

Do I need a Tailwind config file to build this in 2026?

Not with Tailwind v4. You configure everything via @theme in your CSS file. The old tailwind.config.js is optional and mostly used for plugin compatibility.

How do I handle the monthly/yearly pricing toggle state across sections?

Lift the annual boolean to your page-level component and pass it as a prop to <PricingSection>. Don't reach for a global store for something this simple.

Can I use these components with shadcn/ui or Radix primitives?

Yes, cleanly. The FAQ accordion can replace the raw button with Radix <Accordion> for better accessibility primitives. The pricing cards are pure Tailwind and need no changes.

What's the fastest way to make this look less generic?

Change the accent color from violet to something specific to your brand, add a real product screenshot or UI mockup in the hero, and write copy that names a specific pain point. Generic palettes are the main culprit.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Building a Landing Page in Tailwind CSS: Section by SectionTailwind Pricing Section: 3-Tier Layout with Annual ToggleBento Grid SaaS Landing Page: Feature Showcase in 2026 StyleHero Section Design: 8 Layouts With Full React + Tailwind Code