We built a CLI app and a REST API with Claude Code. Both were backend projects. Now let’s see how Claude handles the visual side: a modern landing page with responsive design, animations, and dark mode.

The project: build a landing page for a fictional SaaS product called “FlowBoard” — a project management tool. No React, no frameworks. Just HTML, CSS (Tailwind), and a tiny bit of vanilla JavaScript.

Total time: 38 minutes from empty folder to live on Netlify.

What We’re Building

A modern SaaS landing page with:

  • Hero section with a headline and call-to-action button
  • Features grid showing 6 product features with icons
  • Pricing table with 3 tiers (Free, Pro, Team)
  • Testimonials section
  • Contact form (using Formspree for submissions)
  • Dark/light theme toggle
  • Smooth scroll animations
  • Mobile-first responsive design
  • Deployed on Netlify

Tech stack: HTML5, Tailwind CSS (CDN), vanilla JavaScript, Formspree, Netlify

Prerequisites

  • Claude Code installed and configured
  • A browser (for previewing)
  • A Netlify account (free — for deployment)

The Build Session

Total time: 38 minutes. 5 prompts. Here is the full session.

Prompt 1: Complete Landing Page (Minute 0)

This was a big first prompt. I wanted to see how much Claude could generate at once.

Create a modern landing page for a fictional project management SaaS called
"FlowBoard". Use HTML + Tailwind CSS (CDN link) + vanilla JavaScript.

Include these sections:
1. Navigation bar with logo and links
2. Hero section — big headline, subtext, CTA button, gradient background
3. Features grid — 6 features with SVG icons, title, and description
4. Pricing table — 3 tiers: Free ($0), Pro ($12/mo), Team ($29/mo).
   Each has a feature list and CTA button. Highlight Pro as "Most Popular"
5. Testimonials — 3 testimonial cards with name, role, company, and quote
6. Footer with links and copyright

Design requirements:
- Mobile-first responsive
- Clean, modern design with plenty of whitespace
- Color scheme: indigo/violet primary, gray secondary
- Smooth transitions on hover
- Single index.html file with embedded styles where needed

What Claude generated:

A single index.html file — 280 lines. Claude used the Tailwind CSS CDN and wrote the entire page with Tailwind utility classes.

The hero section:

<section class="relative overflow-hidden bg-gradient-to-br from-indigo-600 via-violet-600 to-purple-700 text-white">
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 sm:py-32">
    <div class="text-center">
      <h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight">
        Manage Projects<br>
        <span class="text-indigo-200">Without the Chaos</span>
      </h1>
      <p class="mt-6 text-lg sm:text-xl text-indigo-100 max-w-2xl mx-auto">
        FlowBoard brings your team's work together. Track tasks, hit deadlines,
        and ship products faster. No learning curve.
      </p>
      <div class="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
        <a href="#pricing" class="rounded-lg bg-white text-indigo-600 px-8 py-3 text-lg font-semibold hover:bg-indigo-50 transition-colors">
          Start Free
        </a>
        <a href="#features" class="rounded-lg border-2 border-white text-white px-8 py-3 text-lg font-semibold hover:bg-white/10 transition-colors">
          See Features
        </a>
      </div>
    </div>
  </div>
</section>

The pricing table was the most impressive part. Claude created three cards with proper visual hierarchy:

<div class="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
  <!-- Free Tier -->
  <div class="rounded-2xl border border-gray-200 p-8 bg-white">
    <h3 class="text-lg font-semibold text-gray-900">Free</h3>
    <p class="mt-2 text-gray-500">For personal projects</p>
    <p class="mt-6"><span class="text-4xl font-bold text-gray-900">$0</span><span class="text-gray-500">/month</span></p>
    <ul class="mt-8 space-y-3 text-sm text-gray-600">
      <li class="flex items-center gap-2">
        <svg class="w-5 h-5 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
        </svg>
        Up to 3 projects
      </li>
      <!-- more features -->
    </ul>
    <a href="#" class="mt-8 block rounded-lg border-2 border-indigo-600 text-indigo-600 text-center py-3 font-semibold hover:bg-indigo-50 transition-colors">
      Get Started
    </a>
  </div>

  <!-- Pro Tier — highlighted -->
  <div class="rounded-2xl border-2 border-indigo-600 p-8 bg-white relative">
    <div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-indigo-600 text-white text-sm font-semibold px-4 py-1 rounded-full">
      Most Popular
    </div>
    <h3 class="text-lg font-semibold text-gray-900">Pro</h3>
    <p class="mt-2 text-gray-500">For growing teams</p>
    <p class="mt-6"><span class="text-4xl font-bold text-gray-900">$12</span><span class="text-gray-500">/month</span></p>
    <!-- features and CTA with solid indigo button -->
  </div>

  <!-- Team Tier -->
  <!-- similar structure -->
</div>

My review: I opened the file in a browser. It looked good. Genuinely good. The color scheme was cohesive, the spacing was generous, and the pricing card with “Most Popular” badge was well-designed.

A few issues:

  • The navigation was not sticky (it scrolled away)
  • No mobile hamburger menu — the nav links just stacked
  • The testimonials section used placeholder text that was too generic

But for a first prompt? This was impressive.

Time spent: 6 minutes (prompt + reading code + browser preview)

Prompt 2: Scroll Animations (Minute 6)

Add smooth scroll-triggered animations. When each section comes into view,
it should fade in and slide up slightly. Use IntersectionObserver API.
No external libraries. Keep it simple — just opacity and transform transitions.

What Claude generated:

Claude added a <script> block at the bottom of the HTML:

document.addEventListener("DOMContentLoaded", () => {
  const sections = document.querySelectorAll("section");

  sections.forEach(section => {
    section.style.opacity = "0";
    section.style.transform = "translateY(30px)";
    section.style.transition = "opacity 0.6s ease-out, transform 0.6s ease-out";
  });

  // Don't hide the hero section
  const hero = document.querySelector("section:first-of-type");
  if (hero) {
    hero.style.opacity = "1";
    hero.style.transform = "translateY(0)";
  }

  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          entry.target.style.opacity = "1";
          entry.target.style.transform = "translateY(0)";
          observer.unobserve(entry.target);
        }
      });
    },
    { threshold: 0.1 }
  );

  sections.forEach(section => observer.observe(section));
});

Clean and functional. The hero section stayed visible while other sections animated in as you scrolled. The unobserve after animation prevented unnecessary re-triggering.

Issue: On fast scrolling, all sections below the fold appeared at once since the observer fired for all visible sections simultaneously. This was fine — the staggered animation was not necessary for this kind of page.

Time spent: 3 minutes

Prompt 3: Dark/Light Theme Toggle (Minute 9)

Add a dark/light theme toggle button in the navigation bar. Use a sun/moon
icon. When in dark mode, change the background to dark gray, text to white,
cards to dark gray. Save the preference in localStorage so it persists.
Use Tailwind's dark mode with class strategy.

What Claude generated:

Claude added the Tailwind dark mode configuration and a toggle button:

<script>
  // Set dark mode from localStorage before page renders (prevents flash)
  if (localStorage.theme === "dark" || (!localStorage.theme && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
    document.documentElement.classList.add("dark");
  }
</script>

And the toggle button in the nav:

<button id="theme-toggle" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
  <svg id="sun-icon" class="w-5 h-5 hidden dark:block text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
    <path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"/>
  </svg>
  <svg id="moon-icon" class="w-5 h-5 block dark:hidden text-gray-600" fill="currentColor" viewBox="0 0 20 20">
    <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
  </svg>
</button>

The toggle script:

document.getElementById("theme-toggle").addEventListener("click", () => {
  const html = document.documentElement;
  html.classList.toggle("dark");
  localStorage.theme = html.classList.contains("dark") ? "dark" : "light";
});

Problem: Tailwind CDN does not support the dark: variant by default. You need to configure it. Claude added the Tailwind config script:

<script>
  tailwind.config = {
    darkMode: "class"
  }
</script>

But this needed to come AFTER the Tailwind CDN script tag. Claude put it in the right order. However, many of the existing Tailwind classes needed dark: variants added. Claude updated the major sections but missed some elements:

  • The pricing cards had white backgrounds in dark mode (hard to read)
  • Some text remained dark gray on dark backgrounds
  • The “Most Popular” badge background needed adjustment

I fixed this with a follow-up:

Several elements are missing dark mode styles. Fix: pricing cards should have
dark:bg-gray-800 backgrounds, all text-gray-600 elements need dark:text-gray-300,
the feature grid section background needs dark:bg-gray-900. Check all sections.

Claude went through every section and added the missing dark variants. After the fix, both modes looked good.

Time spent: 8 minutes (including fixes)

Prompt 4: Mobile Navigation (Minute 17)

Add a hamburger menu for mobile screens. Hide the nav links on small screens
and show a hamburger icon. When clicked, show the links in a vertical dropdown.
Animate the hamburger to X icon on toggle. Close the menu when a link is clicked.

What Claude generated:

<button id="mobile-menu-btn" class="md:hidden p-2">
  <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
    <path id="menu-open" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
    <path id="menu-close" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
  </svg>
</button>

<div id="mobile-menu" class="hidden md:hidden bg-white dark:bg-gray-900 border-t dark:border-gray-800">
  <div class="px-4 py-4 space-y-3">
    <a href="#features" class="block text-gray-600 dark:text-gray-300 hover:text-indigo-600">Features</a>
    <a href="#pricing" class="block text-gray-600 dark:text-gray-300 hover:text-indigo-600">Pricing</a>
    <a href="#testimonials" class="block text-gray-600 dark:text-gray-300 hover:text-indigo-600">Testimonials</a>
    <a href="#contact" class="block text-gray-600 dark:text-gray-300 hover:text-indigo-600">Contact</a>
  </div>
</div>

And the JavaScript:

const menuBtn = document.getElementById("mobile-menu-btn");
const mobileMenu = document.getElementById("mobile-menu");
const menuOpen = document.getElementById("menu-open");
const menuClose = document.getElementById("menu-close");

menuBtn.addEventListener("click", () => {
  mobileMenu.classList.toggle("hidden");
  menuOpen.classList.toggle("hidden");
  menuClose.classList.toggle("hidden");
});

// Close menu when a link is clicked
mobileMenu.querySelectorAll("a").forEach(link => {
  link.addEventListener("click", () => {
    mobileMenu.classList.add("hidden");
    menuOpen.classList.remove("hidden");
    menuClose.classList.add("hidden");
  });
});

Worked well. The hamburger icon toggled to an X, the menu slid down, and clicking a link closed it. Claude also made the nav sticky while it was at it:

<nav class="sticky top-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-800">

The backdrop-blur-md effect with semi-transparent background was a nice touch. The nav felt modern and polished.

Time spent: 5 minutes

Prompt 5: Contact Form and Lighthouse Optimization (Minute 22)

Add two things:
1. A contact form at the bottom that submits to Formspree. Fields: name,
   email, message. Simple validation. Show a success message after submit.
2. Optimize for performance: add proper meta tags (viewport, description,
   Open Graph), lazy load any images, add preconnect for external resources,
   and minimize layout shift.

What Claude generated:

The contact form:

<section id="contact" class="py-20 bg-gray-50 dark:bg-gray-800">
  <div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
    <h2 class="text-3xl font-bold text-center text-gray-900 dark:text-white">Get in Touch</h2>
    <p class="mt-4 text-center text-gray-600 dark:text-gray-300">Have a question? We would love to hear from you.</p>

    <form id="contact-form" action="https://formspree.io/f/YOUR_FORM_ID" method="POST" class="mt-10 space-y-6">
      <div>
        <label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
        <input type="text" name="name" id="name" required
          class="mt-1 block w-full rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-4 py-3 focus:ring-2 focus:ring-indigo-500 focus:border-transparent">
      </div>
      <div>
        <label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Email</label>
        <input type="email" name="email" id="email" required
          class="mt-1 block w-full rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-4 py-3 focus:ring-2 focus:ring-indigo-500 focus:border-transparent">
      </div>
      <div>
        <label for="message" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Message</label>
        <textarea name="message" id="message" rows="5" required
          class="mt-1 block w-full rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-4 py-3 focus:ring-2 focus:ring-indigo-500 focus:border-transparent"></textarea>
      </div>
      <button type="submit"
        class="w-full rounded-lg bg-indigo-600 text-white py-3 font-semibold hover:bg-indigo-700 transition-colors">
        Send Message
      </button>
    </form>
    <div id="form-success" class="hidden mt-6 text-center text-green-600 dark:text-green-400 font-semibold">
      Thank you! We will get back to you soon.
    </div>
  </div>
</section>

The form submission handler:

document.getElementById("contact-form").addEventListener("submit", async (e) => {
  e.preventDefault();
  const form = e.target;
  const data = new FormData(form);

  try {
    const response = await fetch(form.action, {
      method: "POST",
      body: data,
      headers: { "Accept": "application/json" }
    });

    if (response.ok) {
      form.reset();
      form.classList.add("hidden");
      document.getElementById("form-success").classList.remove("hidden");
    }
  } catch (error) {
    console.error("Form submission error:", error);
  }
});

For SEO and performance, Claude added to the <head>:

<meta name="description" content="FlowBoard — Project management without the chaos. Track tasks, hit deadlines, and ship products faster.">
<meta property="og:title" content="FlowBoard — Project Management Made Simple">
<meta property="og:description" content="Track tasks, hit deadlines, and ship products faster. No learning curve.">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">
<link rel="preconnect" href="https://cdn.tailwindcss.com">

Time spent: 6 minutes

After all prompts were done, I spent 10 more minutes doing a final review, testing responsive behavior at different screen widths, and deploying to Netlify.

Deployment was simple:

# Netlify CLI
npm install -g netlify-cli
netlify init
netlify deploy --prod

The site was live in 2 minutes.

Total time: 38 minutes.

What Went Right

  • The first prompt generated a complete, usable page. 280 lines of well-structured HTML with proper Tailwind classes. It would have taken me at least 2 hours to hand-code this.
  • The design was genuinely good. Not just functional — it looked like a real SaaS landing page. The color scheme, spacing, and typography were all appropriate.
  • Tailwind CDN keeps things simple. No build step, no npm, no config. Just an HTML file you open in a browser. Perfect for quick projects.
  • The mobile experience was solid. Hamburger menu, responsive grid, properly sized text. Claude handled responsive design well.

What Went Wrong

1. Incomplete Dark Mode Styles

Claude added dark mode support but missed many elements. Text on pricing cards was unreadable in dark mode because the card backgrounds stayed white. This required a full review and a fix prompt.

Lesson: Dark mode is hard. When you ask for dark mode, specifically ask Claude to “check every section for dark mode contrast.” Better yet, include dark mode in the first prompt so all classes are generated correctly from the start.

2. Generic Testimonial Content

The testimonials Claude wrote were too generic: “Great product, highly recommend.” Real landing pages need specific, believable testimonials. I had to manually rewrite these to sound authentic.

Lesson: For content-heavy sections, give Claude specific examples or personas. “Write a testimonial from a startup CTO who manages a 5-person team and used to use Trello.”

3. No Real Images

The page had no images — just colored sections and SVG icons. A real landing page needs hero images, product screenshots, or illustrations. Claude cannot generate images, so this is a limitation of the approach.

Lesson: AI can build the structure and styling. You still need to add visual assets yourself. Tools like Figma, screenshots of a real product, or stock photos fill this gap.

4. Tailwind CDN in Production

Using the Tailwind CDN is fine for prototyping but not ideal for production. It loads the entire Tailwind library (300+ KB) instead of just the classes you use. For a real production page, you would want a build step with PostCSS to purge unused styles.

Lesson: For prototypes and demos, CDN is perfect. For production, add a build step or switch to a framework that handles this automatically.

The Final Result

A fully responsive SaaS landing page with:

  • 6 sections (nav, hero, features, pricing, testimonials, contact)
  • Dark/light theme toggle with localStorage persistence
  • Scroll animations using IntersectionObserver
  • Mobile hamburger menu
  • Contact form with Formspree integration
  • Meta tags for SEO and social sharing
  • Deployed on Netlify

The page loads fast (no heavy assets), works on all screen sizes, and looks professional. Not perfect for a real SaaS launch, but a strong starting point that would take minimal polish to ship.

Time and Cost

  • Total time: 38 minutes
  • Claude prompts: 5 (plus 2 fixes)
  • Estimated token usage: ~20,000 tokens
  • Hosting cost: $0 (Netlify free tier)
  • Dependencies: None (Tailwind CDN, vanilla JS)

Lighthouse Score

I ran Lighthouse on the deployed page. Here are the scores:

  • Performance: 95/100 — Fast because there are no images or heavy assets. The only external resource is the Tailwind CDN.
  • Accessibility: 88/100 — Lost points for missing aria-label on some buttons and insufficient color contrast on a few secondary text elements.
  • Best Practices: 92/100 — Minor issue with console.log statements left in the JavaScript.
  • SEO: 100/100 — All meta tags in place, semantic HTML, proper heading hierarchy.

For a 38-minute build, these scores are impressive. The accessibility issues could be fixed with one more Claude prompt: “Fix all Lighthouse accessibility warnings — add aria-labels to buttons and improve color contrast on secondary text.”

When to Use This Approach

Building landing pages with Claude Code is one of the strongest use cases for vibe coding. Here is when it works best:

Great for:

  • MVP landing pages to validate an idea
  • Internal project pages that need to look professional quickly
  • Personal portfolio sites
  • Event or conference pages
  • Side project marketing pages

Not great for:

  • Complex web applications with dynamic data
  • Pages that need pixel-perfect design matching a Figma mockup
  • E-commerce sites with complex state management
  • Sites that need CMS integration

The sweet spot is “needs to look good, does not need to be perfect, and needs to be done today.” That describes most landing pages.

Prompt Template for Landing Pages

Based on this session, here is a reusable prompt template for any landing page:

Create a modern landing page for [PRODUCT_NAME]  [ONE_LINE_DESCRIPTION].
Use HTML + Tailwind CSS (CDN) + vanilla JavaScript.

Sections:
1. Sticky nav with logo, links, and dark mode toggle
2. Hero  gradient background, headline, subtext, 2 CTA buttons
3. Features grid  [NUMBER] features with SVG icons
4. Pricing  [NUMBER] tiers: [LIST_PRICES]. Highlight [TIER] as popular
5. Testimonials  [NUMBER] cards with name, role, quote
6. Contact form (Formspree)
7. Footer

Requirements:
- Mobile-first responsive with hamburger menu
- Dark mode with class strategy and localStorage persistence
- Scroll animations with IntersectionObserver
- All dark: variants included from the start
- Color scheme: [PRIMARY_COLOR] primary

This template eliminates most of the issues I encountered. The key additions are “all dark: variants included from the start” and “mobile-first with hamburger menu.”

Source Code

Full source code: kemalcodes/vibe-coding-projects (branch: landing-page)

What’s Next?

In the next article, we step into browser extension territory: Build a Chrome Extension with Claude — Tab Manager. We will see how Claude handles Chrome APIs, Manifest V3, and the extension architecture. It is a very different type of project from what we have built so far.