DEV Community

Cover image for How I Built a Premium SaaS Landing Page Template with Tailwind CSS and Vanilla JS
Abdelkarim Omar Mtit
Abdelkarim Omar Mtit

Posted on

How I Built a Premium SaaS Landing Page Template with Tailwind CSS and Vanilla JS

Building landing pages for SaaS products is repetitive. Hero section, feature grid, pricing table, FAQ — the same pattern every time. So I built a template once, properly, and turned it into a product.

Here's how I approached it and what I learned along the way.

The Goal

Build a SaaS landing page template that:

  • Looks premium out of the box
  • Works without any build tools or frameworks
  • Supports dark mode and multiple color themes
  • Is modular enough to customize quickly

Tech Stack

I kept it intentionally simple:

HTML5 + Tailwind CSS (CDN) + Vanilla JavaScript
Enter fullscreen mode Exit fullscreen mode

Why no React/Next.js?

The buyer should be able to double-click index.html and see the full page. No terminal, no npm install, no configuration. The simpler the setup, the wider the audience.

Why Tailwind via CDN?

For a template product, the Play CDN is perfect. Buyers can change classes directly in the HTML and see results instantly. For production, they can switch to a build setup later.

The Sections

The template has 11 sections, each as a standalone component:

# Section Key Feature
01 Navbar Sticky, scroll-aware, mobile slide-out menu
02 Hero Dashboard mockup with metric cards and chart
03 Logo Bar Infinite scroll marquee with fade masks
04 Features 6-card grid with icons and hover effects
05 Stats Animated counters (Intersection Observer)
06 Showcase Alternating image/text with slide animations
07 Testimonials Star ratings, avatars, quotes
08 Pricing Monthly/yearly toggle with animated switch
09 FAQ Accordion with smooth expand/collapse
10 CTA Gradient background with glow effects
11 Footer Multi-column links, social icons

Each section lives in its own file in a /sections/ folder, so you can mix and match.

Dark Mode Implementation

Dark mode was the most time-consuming feature. Here's the approach:

CSS Custom Properties for all colors:

:root {
  --bg-primary: #ffffff;
  --text-primary: #111827;
  --border-default: #e5e7eb;
}

[data-theme="dark"] {
  --bg-primary: #0b1120;
  --text-primary: #f1f5f9;
  --border-default: #1e293b;
}
Enter fullscreen mode Exit fullscreen mode

JavaScript for the toggle:

function getTheme() {
  return localStorage.getItem('theme-mode') 
    || (window.matchMedia('(prefers-color-scheme: dark)').matches 
      ? 'dark' : 'light');
}

// Apply immediately to prevent flash
document.documentElement.setAttribute('data-theme', getTheme());
Enter fullscreen mode Exit fullscreen mode

Key lessons:

  • Apply the theme before DOM load (in the <head>) to prevent a white flash
  • Use localStorage to remember the user's choice
  • Listen for system preference changes with matchMedia

Color Themes

Four themes — Blue, Purple, Green, Orange — each defined in a tiny CSS file that only overrides --primary-50 through --primary-900:

/* themes/purple.css */
:root {
  --primary-50:  #f5f3ff;
  --primary-500: #8b5cf6;
  --primary-600: #7c3aed;
  --primary-900: #4c1d95;
}
Enter fullscreen mode Exit fullscreen mode

Switching themes means loading a different CSS file. A floating widget in the corner lets you preview all themes live.

Scroll Animations

All animations use the Intersection Observer API — no libraries:

var observer = new IntersectionObserver(function(entries) {
  entries.forEach(function(entry) {
    if (entry.isIntersecting) {
      entry.target.classList.add('animate-in');
      observer.unobserve(entry.target);
    }
  });
}, { threshold: 0.1 });

document.querySelectorAll('[data-animate]').forEach(function(el) {
  observer.observe(el);
});
Enter fullscreen mode Exit fullscreen mode

Elements start with opacity: 0 and transform: translateY(24px), then transition to their final position when they enter the viewport. Simple, performant, no dependencies.

Pricing Toggle

The monthly/yearly pricing toggle uses a CSS-only animated switch with JavaScript to swap the visible prices:

pricingToggle.addEventListener('click', function() {
  this.classList.toggle('active');
  var isYearly = this.classList.contains('active');

  document.querySelectorAll('[data-price-monthly]').forEach(function(el) {
    el.style.display = isYearly ? 'none' : '';
  });

  document.querySelectorAll('[data-price-yearly]').forEach(function(el) {
    el.style.display = isYearly ? '' : 'none';
  });
});
Enter fullscreen mode Exit fullscreen mode

File Structure

stackly/
├── index.html              # Complete landing page
├── 404.html                # Custom 404 page
├── LICENSE.md
├── assets/
│   ├── css/
│   │   ├── styles.css      # Design system + animations
│   │   └── themes/         # 4 color themes
│   └── js/
│       ├── app.js          # Menu, pricing, FAQ
│       ├── animations.js   # Scroll reveal + counters
│       └── theme-switcher.js
├── sections/               # 11 standalone components
│   ├── 01-navbar.html
│   ├── 02-hero.html
│   └── ...
└── docs/
    ├── README.md
    └── CUSTOMIZATION.md
Enter fullscreen mode Exit fullscreen mode

What I'd Do Differently

  1. Start with dark mode from day one. Retrofitting dark mode onto an existing design doubles the CSS work.
  2. Build the theme system first. CSS custom properties should be the foundation, not an afterthought.
  3. Write docs while building. It's much harder to document code you wrote two weeks ago.

Try It

I'd love to hear your thoughts. What would you add or change?


Built by ctrlCheese — We build software that lasts.

Top comments (0)