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