Start with the user task
Progressive enhancement begins by identifying what the page must let a person do: read an article, follow navigation, submit a form, buy an item, or recover from an error. Write that task as an outcome before choosing components. The first implementation should use HTML elements whose native behavior already supports the outcome.
Use links for destinations, buttons for actions, headings for structure, labels for controls, and forms for submission. Native elements bring browser behavior, keyboard support, and semantics that are costly to reconstruct. CSS and JavaScript can improve presentation and convenience after the document has a coherent reading and interaction order.
Build the complete HTML path
Make every important destination addressable with a URL. Server-render or pre-render meaningful content so a delayed script does not leave an empty shell. Forms should have a real action and method, and the server should validate every submission. If client validation runs, it is an early feedback layer rather than the authority.
Design response pages and error states as part of the flow. Preserve safe user input when validation fails, place a clear summary near the start of the form, and connect field messages to their controls. A successful response should follow only an actual accepted operation, not the absence of a JavaScript exception.
Add CSS without hiding structure
Build layout from normal document flow, then add grid or flexbox where relationships require them. Check narrow screens before introducing breakpoints. Fixed widths, unbreakable strings, and positioned decoration are common sources of horizontal overflow. Let content determine height, and reserve space for media with intrinsic dimensions or aspect ratio.
Keep focus indicators visible and interactive targets comfortably usable by touch. Respect reduced-motion preferences for nonessential animation. When newer CSS materially improves the design, place it behind feature queries or use it so unsupported declarations fall away while the underlying layout remains readable.
Use JavaScript for enhancement
Add scripts where they reduce effort: opening a compact mobile menu, filtering an already rendered list, or reporting submission progress. Read initial state from the document or URL, and write shareable state back to the URL when it changes what the page shows. Avoid replacing native history and navigation unless the product truly requires application routing.
Initialize defensively. A missing element or unsupported API should disable one enhancement rather than stop every script. Attach behavior after confirming the required elements exist. For disclosure controls, keep expanded state synchronized and support keyboard interaction expected for the underlying button. Escape should close temporary overlays and return focus deliberately.
Test the seams and failure modes
Test first with JavaScript disabled: content, links, destinations, and form submission should still make sense. Then test keyboard-only operation, zoom, narrow viewports, reduced motion, slow loading, failed requests, and direct entry to fragment URLs. These checks expose assumptions that a standard happy-path click-through misses.
Finally, test enhancements against the base path. A filter needs a visible empty state; a menu must not trap focus; a loading state must clear after errors; back and forward navigation must restore understandable state. Resilience is not an extra fallback page. It is the result of each layer keeping the guarantees made by the layer below it.
Sources & further reading
Technical references checked on 17 September 2026.