Building a landing page for a startup waitlist doesn’t have to mean wrestling with bloated styling frameworks or scattered content files. When developer Maya Bhandari launched Orbit—a Next.js 15 template—she prioritized clean code, zero dependencies, and single-file customization for buyers. Here’s how she made every technical choice count.
Why CSS Modules beat Tailwind in a product template
Most Next.js templates rely on Tailwind CSS for styling, but Maya chose CSS Modules for Orbit. Her reasoning was simple: buyers want readable, editable CSS without memorizing utility classes or waiting for compilation.
CSS Modules provide locally scoped class names by default, preventing naming conflicts across components. They use standard CSS syntax, making the code immediately familiar to developers who may not use utility-first frameworks daily. Best of all, they introduce no runtime overhead—just pure CSS that works out of the box with Next.js 15.
The trade-off? More verbose code for repetitive utilities like spacing or colors. But for a product sold on clarity and maintainability, that verbosity is a feature, not a bug. Buyers can open a file, read the styles, and tweak them without diving into configuration or build processes.
The bento grid: one pixel, zero borders, perfect alignment
The features section in Orbit uses a clean grid layout with a clever CSS trick: the 1-pixel gap between cards is the border. Instead of adding borders to individual cards, Maya set the grid’s background to a subtle border color and used a 1px gap. The result? Crisp, consistent grid lines without extra markup or performance cost.
.bento {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: var(--color-border-subtle);
border-radius: var(--radius-lg);
overflow: hidden;
}
.bento .card:first-child {
grid-column: span 2;
}The first card spans two columns, creating a bold visual hierarchy. The gap’s background color blends seamlessly with the grid’s rounded corners, eliminating the need for border-radius hacks or pseudo-elements. It’s a small detail that elevates the entire design.
Count-up metrics that feel natural (no libraries required)
Animating numbers on scroll can feel jarring if done poorly. Maya implemented a count-up effect in Orbit using the IntersectionObserver API and a cubic easing curve—no external libraries, no bloated dependencies.
The animation triggers when the metrics section enters the viewport. A ref tracks whether the animation has started to prevent re-triggering if users scroll away and back. The easing function uses a cubic out curve, which feels smoother than linear progression and avoids the abrupt “robot counting” effect.
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && !started.current) {
started.current = true;
const startTime = performance.now();
const tick = (now: number) => {
const progress = Math.min((now - startTime) / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3); // cubic ease-out
setCount(Math.round(eased * end));
if (progress < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}
}, { threshold: 0.4 });The entire animation runs in under 30 lines of TypeScript, with no impact on bundle size. It’s a testament to how modern browser APIs can replace heavy libraries for common UI patterns.
Logo marquee that loops seamlessly (CSS-only magic)
A scrolling logo strip is a classic landing page staple, but most implementations rely on JavaScript or heavy CSS animations. Maya built one using pure CSS, with a trick to ensure seamless looping.
The marquee uses a flex container with duplicated logo arrays. The animation translates the entire track by -50% over 24 seconds, creating the illusion of an endless loop. A mask gradient on the parent container fades the edges, preventing a visible “reset” when the animation restarts.
.row {
display: flex;
gap: 64px;
width: max-content;
animation: marquee 24s linear infinite;
}
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.track {
mask-image: linear-gradient(
to right,
transparent 0%,
black 12%,
black 88%,
transparent 100%
);
}No JavaScript means no performance penalties, and the loop is buttery smooth even on low-end devices. It’s a small but delightful detail that reinforces the template’s focus on performance.
One file to rule them all: centralizing content
Landing page templates often scatter editable content across multiple components—navigation links in one file, features in another, testimonials in a third. Maya consolidated everything into src/lib/constants.ts, a single source of truth for all purchasers.
Buyers only need to edit this file to rebrand Orbit. Change a company name, swap a logo, update metrics, or adjust the FAQ—all handled in one place. No hunting through nested folders or deciphering component props.
The approach reduces friction for users who want a polished landing page without deep customization. It also makes the template easier to maintain, as updates to the content schema propagate automatically.
Design tokens: rebranding in one line
Orbit’s visual identity is controlled by eight CSS variables in globals.css. Need to change the accent color? Update --color-accent and the entire template rebrands instantly. The variables cover everything from typography to border radii, ensuring consistency across the page.
:root {
--color-accent: #f59e0b;
--color-bg: #09090b;
--font-display: 'Sora', sans-serif;
--font-body: 'IBM Plex Sans', sans-serif;
--radius-lg: 16px;
}This design system approach makes the template adaptable to any brand, from minimalist startups to bold consumer products. It’s a level of flexibility that most templates lack without requiring buyers to dive into complex theming systems.
Next steps for your waitlist integration
The waitlist form in Orbit ships with a simulated delay, but it’s ready to plug into real email tools like ConvertKit or Loops. Replace the mock API calls with your actual endpoint, and you’re live.
/* ConvertKit example */
await fetch(` {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ api_key: KEY, email }),
});
/* Loops.so example */
await fetch(' {
method: 'POST',
headers: {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});Maya’s Orbit template proves that a landing page doesn’t need to sacrifice performance for polish. By choosing simple, dependency-free techniques and centralizing customization, she created a product that’s both developer-friendly and buyer-ready. Whether you’re launching a startup or selling templates, the principles behind Orbit are worth stealing.
AI summary
Next.js 15'in sunduğu yeniliklerle startup projeleriniz için etkileyici bir bekleme listesi sayfası oluşturun. CSS Modülleri, bento grid, animasyonlar ve içerik yönetimi hakkında detaylı rehber.