Progressive Enhancement Strategy
Build resilient web experiences using progressive enhancement HTML-first foundations that gracefully upgrade with CSS and JavaScript.
Overview
Key Points
- •Layer 1 HTML: semantic, meaningful content that works without CSS or JS
- •Layer 2 CSS: visual enhancement, layout, animation (can be blocked by content filters)
- •Layer 3 JavaScript: behavioral enhancement (can fail, take time to load, or be blocked)
- •Feature detection over user-agent sniffing: `if ("share" in navigator)` not `if (isMobile())`
- •`@supports` in CSS enables progressive CSS enhancement: `@supports (display: grid) { ... }`
- •Use HTML-native features first: `<details>`, `<dialog>`, `<input type="date">` before building custom
- •`<noscript>` tag renders its content when JavaScript is disabled use for critical fallbacks
Examples
Building a form with progressive enhancement
<!-- Layer 1: Pure HTML form works with no CSS/JS -->
<form action="/subscribe" method="POST">
<label for="email">Email address</label>
<input
id="email"
name="email"
type="email"
required
placeholder="you@example.com"
autocomplete="email"
>
<button type="submit">Subscribe</button>
</form>
<!--
The above form:
- Submits to the server with no JS
- Has native email validation (type="email" + required)
- Works with screen readers (label association)
- Works in email parsers, RSS readers, bots
-->
<script>
// Layer 3: Enhance with AJAX submission
const form = document.querySelector('form[action="/subscribe"]');
form?.addEventListener('submit', async (e) => {
e.preventDefault(); // Only intercept if JS works
const formData = new FormData(form);
try {
const res = await fetch(form.action, { method: 'POST', body: formData });
if (res.ok) showSuccessMessage();
else throw new Error('Server error');
} catch {
// On failure: submit normally (form still has action="/subscribe")
form.submit();
}
});
</script>If the JavaScript fails for any reason, the form falls back to a normal server POST it still works.
CSS @supports and feature-based enhancement
/* Layer 2: Base styles work everywhere */
.card-list {
margin: 0;
padding: 0;
list-style: none;
}
.card {
display: block;
border: 1px solid #e2e8f0;
padding: 1rem;
margin-bottom: 1rem;
}
/* Progressive CSS: enhance with Grid if supported */
@supports (display: grid) {
.card-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.card { margin-bottom: 0; }
}
/* Enhance further with Container Queries if supported */
@supports (container-type: inline-size) {
.card {
container-type: inline-size;
}
@container (min-width: 400px) {
.card { display: flex; gap: 1rem; }
}
}@supports ensures grid layout only applies in browsers that understand it older browsers get the stacked block layout.
JavaScript feature detection and graceful API adoption
<!-- noscript fallback for no-JS environments -->
<noscript>
<div class="no-js-message">
<p>Please enable JavaScript for the full experience.</p>
<p>
<a href="/static-version">View the static version instead</a>
</p>
</div>
</noscript>
<script>
// Feature detection check the API exists before using it
async function shareContent(data) {
if ('share' in navigator && navigator.canShare?.(data)) {
// Use native share
await navigator.share(data);
} else if ('clipboard' in navigator) {
// Fallback: copy URL
await navigator.clipboard.writeText(data.url);
showToast('Link copied!');
} else {
// Last resort: show a share menu with links
showCustomShareMenu(data.url);
}
}
// Use CSS-powered accordion first, enhance with animation
document.querySelectorAll('details').forEach(details => {
// details already works without JS (toggle built in)
// JS enhancement: add smooth animation
if (!window.matchMedia('(prefers-reduced-motion: no-preference)').matches) {
return; // skip animation for motion-sensitive users
}
addSmoothAnimation(details);
});
// Check for CSS animation support before animating
if (CSS.supports('animation', 'slideIn 0.3s ease')) {
document.body.classList.add('animations-enabled');
}
</script>Common Mistakes
- Requiring JavaScript for ANY page content to render search engine crawlers, low-powered devices, and slow connections all suffer
- User-agent sniffing instead of feature detection UAs can be spoofed, lie, or represent capabilities inaccurately
- Treating progressive enhancement as "making it work without JavaScript" it's about layers of reliability, not just a fallback mode
Note
Test progressive enhancement by opening Chrome DevTools > Network tab, right-click > Block request domain > block your JS files. The page should still render meaningful content and forms should still be submittable.
Exercises
Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4