Best Practices

AI Rules for Progressive Enhancement

AI builds JavaScript-dependent apps that show a blank page without JS. Rules for HTML-first rendering, form actions without JS, CSS-only interactions, feature detection, and graceful degradation layers.

7 min read·April 4, 2025

Blank white page without JavaScript — the app does not exist without its enhancement layer

HTML-first rendering, forms without JS, CSS-only interactions, feature detection, three-layer model

AI Builds Apps That Require JavaScript to Exist

AI generates applications where: the page is a blank white screen without JavaScript (a <div id="root"></div> and nothing else), forms do not submit without JavaScript (onClick handler with fetch, no <form action>), links are <div onClick> instead of <a href> (not navigable without JS, not crawlable by search engines), interactive elements are not discoverable without JS (dropdown menus, tabs, and accordions invisible until JavaScript loads), and no consideration for progressive loading (the entire app must download and parse before any content is visible). JavaScript is a requirement, not an enhancement.

Progressive enhancement builds on layers: HTML first (semantic, accessible, functional without CSS or JS), CSS second (layout, visual design, animations — content accessible without CSS), JavaScript third (enhanced interactions, real-time updates, SPA navigation — core functionality works without JS). Modern implementation: React Server Components render HTML on the server (content visible immediately), forms use server actions (submit without JS via native form submission), and interactive components enhance the base HTML when JS loads.

These rules cover: HTML-first server rendering, form actions that work without JavaScript, CSS-only interactive patterns, feature detection with graceful fallbacks, and the progressive enhancement mental model for modern frameworks.

Rule 1: HTML-First Server Rendering

The rule: 'Render meaningful HTML on the server. The page should be readable, navigable, and functional before JavaScript loads. Next.js App Router: Server Components render HTML by default (zero client JS). The page content, navigation links, headings, and images are in the initial HTML response. Client Components enhance with interactivity after hydration. The time between HTML arrival and JavaScript hydration (typically 200-500ms) should not be a blank page — it should be a fully readable page.'

For the no-JS test: 'Disable JavaScript in your browser (Chrome DevTools > Settings > Debugger > Disable JavaScript). Load your page. Can you: read the content? Navigate via links? See images? Read form labels? If yes: your HTML layer is solid. If no: the content is JavaScript-dependent and fails for: search engine crawlers (some do not run JS), users on slow connections (JS is still downloading), users with JS disabled or blocked (corporate proxies, accessibility tools), and the first 200-500ms of every page load (before hydration).'

AI generates: return <div id="root"></div> with client-side rendering. Disable JS: blank page. Google bot on first crawl: blank page (some pages are JS-rendered by Googlebot, but not all, and not immediately). Server rendering with Next.js: the HTML contains the full content. Googlebot sees content immediately. Users on slow connections see content while JS loads. The HTML is the foundation, not a placeholder for JavaScript.

  • Server Components: HTML rendered on server, content visible before JS loads
  • No-JS test: disable JavaScript, verify content is readable and navigable
  • Search crawlers see content immediately — no waiting for JS execution
  • 200-500ms hydration gap: user reads content, not a blank page
  • HTML is the foundation, JavaScript is the enhancement — not the requirement
💡 Readable Before Hydration

200-500ms between HTML arrival and JS hydration. AI: blank page during this gap. Server Components: full content visible immediately. The user reads the article, sees navigation, views images — all before a single byte of JavaScript executes.

Rule 2: Form Actions That Work Without JavaScript

The rule: 'Use <form action={serverAction}> so forms submit via native HTML form submission when JavaScript is unavailable. Next.js server actions: the form POSTs to the server, the server action processes it, and the page redirects or revalidates. With JavaScript: the submission is enhanced (no page reload, optimistic UI, pending states via useFormStatus). Without JavaScript: the form submits normally (page reloads with the result). The core functionality (submitting data) works either way — JS enhances the experience but does not gate it.'

For the enhancement layers: 'Layer 1 (HTML): <form action={createComment}><textarea name="content" required /><button type="submit">Post</button></form>. Works without JS: native form submission, page reloads. Layer 2 (JS enhancement): useFormStatus for pending spinner, useOptimistic for instant UI update, client-side validation for real-time feedback. The HTML form is the foundation. JavaScript adds: no-reload submission, instant feedback, and richer validation. Remove JS: the form still submits. Remove the form: nothing works.'

AI generates: <button onClick={() => { fetch('/api/comments', { method: 'POST', body: JSON.stringify(data) }); }}>Post</button> — no form element, no action, no native submission. Without JS: the button does nothing. It is not even a submit button — it is a div with an onClick. With a proper form + server action: the button submits the form natively. JS enhances with fetch and optimistic UI. The core action works at every layer.

Rule 3: CSS-Only Interactive Patterns

The rule: 'Use CSS for interactions that do not require JavaScript: accordions (<details>/<summary> elements — native expand/collapse), hover effects (CSS :hover for tooltips, menus on desktop), focus states (:focus-visible for keyboard navigation indicators), responsive layout changes (media queries, container queries), and dark mode (prefers-color-scheme media query). These CSS interactions: work without JavaScript, work during the hydration gap, have zero bundle cost, and are often more performant than JS equivalents (CSS animations run on the compositor thread).'

For <details>/<summary>: '<details><summary>FAQ: How does billing work?</summary><p>We charge monthly based on your plan...</p></details>. This is a fully functional accordion with: no JavaScript, no useState, no onClick handler, and no animation library. It works in all modern browsers. Style it with CSS: details[open] summary { font-weight: bold; } details[open] > p { animation: fadeIn 200ms; }. The HTML element IS the interaction. JavaScript can enhance it (smooth animation, tracking opens for analytics) but is not required.'

AI generates: const [isOpen, setIsOpen] = useState(false); return <div><button onClick={() => setIsOpen(!isOpen)}>{isOpen && <p>Content</p>}</button></div> — a custom accordion requiring JavaScript, useState, and an onClick handler. 10 lines of React for what <details>/<summary> does in 3 lines of HTML with zero JS. The HTML element works during hydration, works without JS, and is accessible by default (screen readers announce it as a disclosure widget).

  • <details>/<summary>: native accordion, zero JS, accessible by default
  • CSS :hover for tooltips and dropdown menus on desktop
  • :focus-visible for keyboard navigation indicators — no JS needed
  • prefers-color-scheme: dark mode without JavaScript — CSS media query
  • CSS interactions: zero bundle cost, work during hydration, compositor-thread performance
⚠️ 3 Lines of HTML vs 10 Lines of React

<details><summary>FAQ</summary><p>Answer</p></details> — native accordion, zero JS, accessible by default. AI writes: useState, onClick handler, conditional render, animation — 10 lines for the same result. The HTML element IS the interaction. JS can enhance, but is not needed.

Rule 4: Feature Detection with Fallbacks

The rule: 'Detect features before using them, provide fallbacks when absent. Pattern: if ("IntersectionObserver" in window) { /* use IntersectionObserver for lazy loading */ } else { /* load all images eagerly — the fallback is worse performance, not broken functionality */ }. Feature detection libraries: core-js for polyfills, @supports in CSS for feature queries. Never user-agent sniff ("if Chrome then...") — feature detect ("if IntersectionObserver then..."). User agents lie; feature detection does not.'

For CSS feature queries: '@supports (display: grid) { .layout { display: grid; grid-template-columns: 1fr 2fr; } } @supports not (display: grid) { .layout { display: flex; flex-wrap: wrap; } }. The grid layout is used when supported; the flex fallback works in older browsers. Both layouts display the content correctly — the grid version is more precise. @supports is the CSS equivalent of if-then-else for features. No JavaScript needed for CSS feature detection.'

AI generates: code that uses the latest Web APIs with no checks. A browser that does not support IntersectionObserver: JavaScript error, feature broken, potentially the entire page broken (unhandled exception). With feature detection: the feature degrades gracefully. Lazy loading is not available: all images load eagerly (worse performance, but all images visible). The user on an older browser gets a working page, not a crash.

Rule 5: The Progressive Enhancement Mental Model

The rule: 'Build in layers from the bottom up. Layer 0 (Content): semantic HTML with meaningful text, images, and links. Works everywhere. Layer 1 (Presentation): CSS for layout, typography, colors, and responsive design. Enhances visual experience. Layer 2 (Behavior): JavaScript for dynamic interactions, real-time updates, and SPA navigation. Enhances interactivity. Each layer enhances the previous. Removing a layer degrades the experience but does not break functionality. The content is always accessible.'

For modern framework application: 'Next.js App Router maps to progressive enhancement naturally: Server Components = Layer 0 + 1 (HTML + CSS, server-rendered, zero client JS). Client Components = Layer 2 (JavaScript enhancement for interactivity). Server Actions = forms work at Layer 0 (native submission), enhanced at Layer 2 (no-reload, optimistic UI). Streaming SSR = content arrives progressively (Layer 0 first, Layer 2 hydrates after). The framework architecture IS progressive enhancement when used correctly.'

AI generates: everything at Layer 2 (JavaScript renders the HTML, CSS, and content). Remove Layer 2: nothing exists. The app has no Layer 0 or Layer 1 — it is JavaScript all the way down. With progressive enhancement: Layer 0 (HTML content) exists independently. Add Layer 1 (CSS styling) for visual improvement. Add Layer 2 (JS interactivity) for dynamic features. Each layer is independently valuable. The user gets the best experience their environment supports.

ℹ️ Each Layer Independently Valuable

AI builds everything at Layer 2 (JS). Remove JS: nothing exists. Progressive enhancement: Layer 0 (HTML content) works alone. Add Layer 1 (CSS) for visual improvement. Add Layer 2 (JS) for interactivity. Each layer enhances. No layer is a single point of failure.

Complete Progressive Enhancement Rules Template

Consolidated rules for progressive enhancement.

  • HTML-first: server-render content, readable and navigable before JS loads
  • No-JS test: disable JavaScript, verify core functionality works
  • Forms with server actions: <form action={fn}> works without JS, enhanced with JS
  • CSS-only interactions: <details>, :hover, :focus-visible, @supports, prefers-color-scheme
  • Feature detection: if ('feature' in window), @supports in CSS — never user-agent sniffing
  • Graceful fallback: degraded performance or simpler UI, never broken functionality
  • Three layers: HTML (content) + CSS (presentation) + JS (behavior) — each independently valuable
  • Next.js App Router: Server Components = Layer 0+1, Client Components = Layer 2