A marketing landing page looks deceptively simple — a hero, some features, a pricing table, a call to action. But the gap between "renders" and "looks designed" is almost entirely craft: a consistent type scale, a real spacing system, breakpoints chosen on purpose, and behaviour that holds up under a keyboard and a screen reader. This is a walk through how I build one by hand in semantic HTML5 and modern CSS3, framework-free, so it deploys as a static site and runs anywhere.

Start with a token system, not a stylesheet

Before a single component, I define the design system as CSS custom properties: color, a fluid type scale, a spacing ramp, radii, and shadows. Everything downstream references these tokens, so the page stays internally consistent and a rebrand is a five-line diff instead of a find-and-replace across hundreds of declarations. The type scale is the part people skip and the part that most separates "pixel-perfect" from "AI-generated default" — I use a minor-third ratio and make it fluid with clamp() so headings breathe between phone and desktop without a dozen breakpoint overrides.

:root {
  --fs-700: clamp(2.25rem, 1.7rem + 2.6vw, 3.6rem);  /* hero */
  --fs-400: clamp(1.1rem,  1.02rem + 0.4vw, 1.3rem); /* lede */
  --sp-5: 1.5rem;  --sp-7: 3rem;                      /* spacing ramp */
  --c-accent: #e8833a;
}
.hero-title { font-size: var(--fs-700); letter-spacing: -0.02em; }

Mobile-first, with breakpoints you can defend

The base styles target the smallest screen — roughly 360px — and every breakpoint is a min-width query that adds capability as there's room for it: two-up feature cards at 640px, a two-column hero and a three-tier pricing grid at 768px, three-up features at 1024px. Writing it this way means the mobile layout is never an afterthought patched on with overrides; it's the default, and the desktop is the enhancement. The one place I reach for a max-width query is collapsing the navigation into a hamburger panel, because that's genuinely a different component below the fold-line of a phone, not just a reflow.

CSS grid does the heavy structural work and flexbox handles the one-dimensional rows. Because each grid is defined per breakpoint, I can verify the page at 360, 768, and 1280px and know exactly which rule is responsible for each layout — which is also what makes it painless to debug in Chrome DevTools.

Just enough JavaScript — and make it accessible

A marketing page needs only a little interactivity: a mobile menu, a FAQ accordion, smooth in-page scrolling, and inline form validation. I write it as a small vanilla ES6+ module — no framework tax for four interactions — but the accessibility is non-negotiable. The accordion is a real disclosure pattern: each trigger is a <button> with aria-expanded and aria-controls, each panel toggles the hidden attribute, and arrow keys move focus between headers. The mobile menu closes on Esc and returns focus to the toggle. The email field has a real (visually hidden) label and an aria-live error region instead of the browser's default validation bubble.

Motion respects the user: every transition is gated behind prefers-reduced-motion, and smooth-scroll falls back to an instant jump when that's set. None of this shows up in a screenshot, but all of it shows up the moment a real person uses the page with a keyboard.

Cross-browser is a discipline, not a library

Framework-free doesn't mean fragile. Web fonts are progressive enhancement with a full system-font fallback stack, so a blocked CDN degrades gracefully instead of flashing invisible text. backdrop-filter and CSS mask carry their -webkit- prefixes, and a forced-colors query keeps the UI legible in high-contrast mode. The whole thing is debuggable end to end in DevTools because there's no build output between what I wrote and what ships.

The same instinct — ship the smallest correct thing and lean on the platform — carries past the landing page. I've written about driving real-time UI without a SPA at all using Turbo Streams, and about pushing layout decisions to a server-driven UI runtime across four platforms. Different scales, same principle: semantics and the platform first, framework only when it earns its weight. A landing page that's hand-built to this standard loads fast, ranks well, and — most importantly for a marketing team — is trivial for the next person to pick up and edit.

Run it

The full source is on GitHub — github.com/tachyurgy/responsive-landing-kit (MIT). A live version of the page is here. Cloned fresh, it runs with its base toolchain and nothing else. Here is an actual run:

================================================================================
 RESPONSIVE LANDING KIT — RUN & VALIDATION OUTPUT
 Generated: 2026-06-06
================================================================================
Everything below was run locally against the actual files in this directory.
No build step, no dependencies — pure static HTML/CSS/JS served with Python's
standard-library HTTP server.
--------------------------------------------------------------------------------
1. SERVE THE SITE
--------------------------------------------------------------------------------
  $ python3 -m http.server 8000
  Serving HTTP on :: port 8000 (http://[::]:8000/) ...
  (For the test run a non-default port 8123 was used to avoid collisions.)
--------------------------------------------------------------------------------
2. HTTP STATUS — every asset returns 200 with the correct Content-Type
--------------------------------------------------------------------------------
  $ for f in / /styles.css /app.js /jquery-interactions.js /assets/preview.svg; do
      curl -s -o /dev/null -w "%{http_code} %{content_type}  $f\n" "http://localhost:8123$f"
    done
  200 text/html               /
  200 text/css                /styles.css
  200 text/javascript         /app.js
  200 text/javascript         /jquery-interactions.js
  200 image/svg+xml           /assets/preview.svg
--------------------------------------------------------------------------------
3. DOCUMENT STRUCTURE (curl http://localhost:8123/ | grep)
--------------------------------------------------------------------------------
  bytes:               19464
  <section> tags:      7        (hero, logos, features, pricing, testimonials, faq, cta)
  <nav> landmarks:     4        (primary nav + 3 footer navs)
  accordion triggers:  4        (FAQ items)
  aria-expanded attrs: 5        (4 accordion + 1 nav toggle)
  has <main id="main">:  yes    (skip-link target present)
--------------------------------------------------------------------------------
4. JAVASCRIPT — syntax validated with `node --check`
--------------------------------------------------------------------------------
  $ node --check app.js                 -> app.js OK
  $ node --check jquery-interactions.js -> jquery-interactions.js OK
  Both files parse cleanly as valid ECMAScript. app.js is a framework-free
  IIFE (strict mode); jquery-interactions.js is an opt-in jQuery variant and
  guards against a missing jQuery global so it never throws if loaded alone.
--------------------------------------------------------------------------------
5. CSS — brace balance verified
--------------------------------------------------------------------------------
  open  '{' : 162
  close '}' : 162