A marketing page is read once by someone who did not ask to be there, which inverts most product-UI defaults: the animation budget goes up, the information density goes down, and the page proves one claim instead of exposing every capability. Default to a short page — one claim, one primary CTA repeated verbatim, evidence beside each promise rather than pooled at the bottom. The larger motion budget is an allowance, not a licence: anything that moves without the visitor causing it reads as the page performing at them while they are trying to decide. This skill builds one page inside a system it does not own — if the open question is which accent, typeface, illustration voice, or tone the page speaks in, that is brand-identity; here those are already decided and only arranged.
Detect the stack before writing a line. Read how the project already builds pages — framework and rendering mode (static export, ISR, server components), styling system (Tailwind, CSS Modules, custom properties, CSS-in-JS), the existing section and container components, and the animation library if one is present. Write the page in that system. A landing page is the most common place a parallel styling layer or a second animation library gets bolted on, and it is never justified: if the project ships CSS transitions, the hero does not import a motion library for one fade.
Quick Reference
| Topic | File |
|---|---|
| Building or ordering a specific section — hero, logo wall, feature block, demo, testimonial, FAQ, final CTA, or an attached blog/changelog/docs surface. Open it when you know the page needs a section but not what belongs inside it. | section-catalog.md |
| Anything with a price on it: tier count, highlighted plan, annual/monthly toggle, feature comparison table, or a page that has to answer objections. Open it whenever the page shows money. | pricing-and-conversion.md |
Core Principles
- Motion still has to map to user input. No scroll-triggered fade-ups, no scroll hijacking, no auto-advancing carousel, no parallax that is not
1:1with scroll position. Motion the visitor did not cause reads as a performance, and a self-advancing carousel moves the thing being read. Exception: a reveal scrubbed by scroll position — reversible, running backwards when you scroll up — is direct manipulation, not a trigger, and is allowed. - Spend the extra animation budget on one sequence, not on every section. Product UI stays under
300ms(Emil Kowalski's duration table, whose only "can be longer" row is marketing/explanatory). One hero or explanatory sequence may run out to Benji Taylor's published--duration-swift: 800ms / cubic-bezier(.175,.885,.32,1.275). Note the live disagreement: Benji'ssnappyandswiftcurves overshoot past 1, Emil's three all settle — pick one family for the project and never mix them (seemotion/references/techniques.md). Exception: anything clickable stays under the product ceiling. A CTA that takes 800ms to acknowledge a press is broken on any page type. - The intro animation plays once per session. Gate it on
sessionStorage, notlocalStorage— a genuinely new visit should see it, an internal navigation should not. The page animates in once and then stops moving. Exception: underprefers-reduced-motion: reduce, skip the intro entirely, first visit included; reduced motion means gentler, not delayed. - The hero says what it is, who it is for, and shows it working. One claim sentence, one qualifying sentence, one primary CTA, one secondary escape hatch (docs, demo, pricing), and one piece of evidence that is the actual product — not an abstract gradient blob. Cropping, framing, and loading of that evidence belong to
imagery. Exception: a pre-launch or waitlist page with nothing to show — there the email field is the hero. - One primary CTA, same verb every time it appears. "Get started" in the nav and "Try it free" in the footer read as two different offers. Ship
1visually primary action per viewport, repeats identical in wording and destination. CTAs also read auth state: logged out getsGet Started, logged in getsGo to Dashboard— telling a signed-in user to sign up is a dead end. Exception: the pricing page, where each tier card carries its own primary and the hierarchy moves to the highlighted tier. - Proof goes next to the claim it proves. A "trusted by" band at the bottom proves nothing about a feature described three screens above it. Attach the logo, quote, or number to the section making the claim. Exception: a first-viewport logo wall earns its place only when the logos are recognizable to this specific reader; otherwise it reads as filler and belongs below the first feature section.
- Ship
5-7sections and cut the eighth (house default). Each extra section dilutes the one claim and pushes the closing CTA further away. A capability that cannot earn a section belongs in docs. Exception: developer tools, where a code block plus its output often needs one section per concept. - Everything a crawler or screen reader needs is in the DOM at rest. Hover-revealed nav submenus are visually hidden with
aria-expandedon the trigger, never conditionally mounted — mount-on-hover markup does not exist for a crawler and cannot be reached by a keyboard. Exception: a menu large enough to bloat first paint may lazy-mount, but only behind a real<button>, with its links duplicated in the footer. - Pre-render, preload, and lock the space. Content pages are statically generated with revalidation (
export const revalidate = 3600), never fetched at request time; fonts and the above-the-fold hero image are preloaded; headings gettext-wrap: balanceso no title strands a single word. Exception: per-visitor content (the auth-aware CTA, a live counter) hydrates on the client over pre-rendered markup — it does not make the page a request-time render. - Decorative illustrations built from DOM elements need a name and no pointer surface. Give them
role="img"with anaria-label, plususer-select: noneandpointer-events: none, or they get selected mid-drag and announced as a pile of empty divs. Exception: an interactive illustration keeps its pointer events, and then owes a keyboard path too.
Smell / Fix
| Smell | Fix |
|---|---|
| Sections fade up as they enter the viewport | Delete the observer. Render them visible; motion that fires on scroll fires on every scroll direction and fights the reader |
| Hero carousel rotating through three value props | You have three claims and therefore no claim. Pick one, demote the rest to feature sections |
| Nav says "Get started", hero says "Start free trial", footer says "Sign up" | One verb, one destination, repeated verbatim |
| A wall of unfamiliar logos under a "Trusted by" caption | Replace with one named quote plus a number, or move it below the first feature section |
localStorage guarding the intro animation |
sessionStorage — the intro returns on a real new visit |
| Blog or changelog fetched at request time | Static generation plus revalidation; these change hourly at most |
| Webfont swaps in and the hero reflows | Preload the font file and declare a metric-matched fallback stack |
| Pricing table only readable by scrolling sideways on mobile | Reflow to one card per tier — see pricing-and-conversion.md |
| A motion library imported for one hero fade | Use the project's existing transitions; the library outweighs the effect |
<div> with an onClick acting as the CTA |
A real <a> or <button> — the CTA is the one element that must survive a crawler, a middle-click, and a keyboard |
Output Format
When proposing or reviewing a page, emit a section manifest before any code — one row per section, in scroll order:
| # | Section | The one claim it makes | Evidence on screen | CTA |
|---|---|---|---|---|
| 1 | Hero | Deploys in one command | 12-second product recording | primary |
| 2 | Logo wall | Four teams you recognize already use it | 6 marks, greyscale | none |
| 3 | Feature | Rollbacks are instant | Annotated screenshot of the rollback | inline link |
A row with no claim is a section to cut. A row whose evidence reads "illustration" is a claim with no proof. Two primary CTAs with different wording is a defect, not a variant.
Checklist
- One claim, stated in the hero in one sentence, with the product visible
-
5-7sections, each with a claim and its own evidence - One primary CTA, identical wording and destination everywhere it repeats
- CTA copy and href switch on auth state
- No scroll-triggered reveals, scroll hijacking, non-
1:1parallax, or auto-advancing carousel - One curve family across the page; no clickable element animates over
300ms - Intro animation gated on
sessionStorageand skipped underprefers-reduced-motion - Statically generated with revalidation; fonts and hero image preloaded; no layout shift
- Nav submenu markup present in the DOM when closed,
aria-expandedon the trigger -
text-wrap: balanceon headings; code-built illustrations havearia-label,user-select: none,pointer-events: none - Pricing, if present, reviewed against pricing-and-conversion.md