惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
V2EX
J
Java Code Geeks
月光博客
月光博客
博客园_首页
The GitHub Blog
The GitHub Blog
Vercel News
Vercel News
B
Blog RSS Feed
博客园 - 聂微东
宝玉的分享
宝玉的分享
T
Tailwind CSS Blog
Jina AI
Jina AI
S
SegmentFault 最新的问题
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
有赞技术团队
有赞技术团队
Hugging Face - Blog
Hugging Face - Blog
Google DeepMind News
Google DeepMind News
阮一峰的网络日志
阮一峰的网络日志
The Cloudflare Blog
量子位
Martin Fowler
Martin Fowler
博客园 - Franky
大猫的无限游戏
大猫的无限游戏
博客园 - 叶小钗

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
I built a startup waitlist landing page in Next.js 15 — h...
Dev Maya · 2026-05-11 · via DEV Community

Dev Maya

I've been building Next.js templates as a side project and selling them on Gumroad. This weekend I shipped the fourth one: Orbit, a startup launch and waitlist landing page.

Here's a breakdown of every technical decision I made.

Why Next.js 15 with CSS Modules (no Tailwind)

Most templates use Tailwind. That's fine for customization, but it adds a compilation step and a learning curve for buyers who just want clean CSS they can read and edit.

CSS Modules give you:

  • Locally scoped class names (no conflicts)
  • Standard CSS syntax (no utility memorization)
  • Zero runtime cost
  • Works with Next.js out of the box

The tradeoff is more verbose than Tailwind for repetitive utilities. Worth it for a product you're selling.

The bento grid — 1px gap trick

The features section uses CSS Grid with grid-template-columns: repeat(3, 1fr). The first card spans 2 columns via grid-column: span 2.

.bento {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--color-border-subtle); /* gap IS the border */
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.bento .card:first-child {
  grid-column: span 2;
}

Enter fullscreen mode Exit fullscreen mode

Instead of adding borders to each card, I set the grid's background to the border color and use 1px gaps. The cards themselves have no borders. This gives perfectly consistent grid lines with zero extra markup.

Count-up animation with IntersectionObserver

The metrics section triggers a count-up when the section enters the viewport:

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 })

Enter fullscreen mode Exit fullscreen mode

The started ref prevents re-triggering if the user scrolls away and back. Cubic ease-out feels much more natural than linear. No library — 30 lines of TypeScript.

Infinite logo marquee (CSS-only)

.row {
  display: flex;
  gap: 64px;
  width: max-content;
  animation: marquee 24s linear infinite;
}

@keyframes marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

Enter fullscreen mode Exit fullscreen mode

The key: duplicate the logos array in the component and animate exactly -50% (half the total width). Seamless loop. Edge fade via mask-image on the parent:

.track {
  mask-image: linear-gradient(
    to right,
    transparent 0%, black 12%, black 88%, transparent 100%
  );
}

Enter fullscreen mode Exit fullscreen mode

Single content file

All editable content — name, copy, nav links, logos, features, metrics, testimonials, FAQ — lives in src/lib/constants.ts. The buyer touches one file and the whole page updates. No hunting through components.

Design tokens in globals.css

8 variables to rebrand the entire template:

:root {
  --color-accent: #f59e0b;   /* change this → full rebrand */
  --color-bg:     #09090b;
  --font-display: 'Sora', sans-serif;
  --font-body:    'IBM Plex Sans', sans-serif;
  --radius-lg:    16px;
}

Enter fullscreen mode Exit fullscreen mode

Connecting the waitlist form

The form ships with a simulated delay. Replace it with your stack:

/* ConvertKit */
await fetch(`https://api.convertkit.com/v3/forms/${FORM_ID}/subscribe`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ api_key: KEY, email }),
})

/* Loops */
await fetch('https://app.loops.so/api/v1/contacts/create', {
  method: 'POST',
  headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ email }),
})

Enter fullscreen mode Exit fullscreen mode


Live demo: https://orbit-landing-iota.vercel.app/

The template is available on Gumroad for $29: https://devmaya.gumroad.com/l/orbit