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
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;
}
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());
Key lessons:
- Apply the theme before DOM load (in the
<head>) to prevent a white flash - Use
localStorageto 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;
}
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);
});
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';
});
});
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
What I'd Do Differently
- Start with dark mode from day one. Retrofitting dark mode onto an existing design doubles the CSS work.
- Build the theme system first. CSS custom properties should be the foundation, not an afterthought.
- Write docs while building. It's much harder to document code you wrote two weeks ago.
Try It
- Live Preview: lucent-unicorn-2af3ba.netlify.app
- Get the template: ctrlcheese.gumroad.com/l/stackly
I'd love to hear your thoughts. What would you add or change?
Built by ctrlCheese — We build software that lasts.
Top comments (0)