How to Build Cross-Browser Web Apps with Modernizr
Use Modernizr to detect browser capabilities, choose enhanced or fallback paths, and test both experiences across your target browsers.
Build a cross-browser web app by checking for the specific browser capability each enhancement needs, using that enhancement when available, and keeping a complete fallback for everything essential. Modernizr runs feature detects and exposes their results as JavaScript properties and, by default, CSS classes. It detects support; it does not make unsupported features work or replace a polyfill.
This guide uses CSS gradients as a small example. Apply the same pattern to the capabilities your app actually uses: inventory them, select the corresponding detects, build a focused Modernizr bundle, keep the baseline usable, and test both paths in your target browsers. The Modernizr repository says its website is outdated and directs developers to build from npm, so confirm the project’s current package instructions before copying commands.
1. Inventory capabilities and choose fallbacks
Start with your design and code, not a list of browser names. Record each optional capability, what the enhanced experience does, and what the user can still do without it. Pick narrowly scoped detects that correspond to those requirements. Browser family and version are only indirect clues; capability detection asks whether the needed feature is available in the current environment.
| Capability | Enhanced path | Fallback requirement |
|---|---|---|
| CSS gradients | Use a gradient background | Keep readable text over a solid background |
| A feature used by a custom interaction | Enable that interaction when its required behavior is supported | Keep the essential action available through a simpler interaction |
These are planning examples, not a claim that a particular detect or fallback is right for every application. Check the current documentation for the exact detect and its meaning. Do not treat a positive detect as proof that the full user experience is correct: test the enhanced path and the fallback.
2. Install Modernizr and build the detects you need
Use the npm and CLI workflow described by the project repository. A custom build can select feature detects rather than including every available test. The repository also points to an all-features configuration as a reference. Package details can change; check the repository for current installation, CLI, and configuration syntax before using these commands.
# Install the package in your project; confirm the current package instructions first.
npm install modernizr
# Inspect the installed package for its current CLI and configuration guidance.
# Create a configuration selecting only the detects your app needs, then build it
# using the CLI command documented by the installed version.
The research sources do not establish the current package release or exact CLI invocation, so this guide does not present a potentially stale build command as current. Modernizr’s repository documents npm-based programmatic builds and command-line builds. Its README also notes that Node.js 10 and below are no longer supported in its v4 notes; verify the installed version’s actual requirements rather than assuming those notes describe the latest release.
In your build configuration, include the exact detect names used by your app. Treat the repository’s all-features configuration as a reference for available configuration fields, not as a reason to ship every detect. A focused build keeps the generated artifact aligned with your needs.
3. Use feature classes for CSS presentation
By default, Modernizr adds a class to the root HTML element for a supported detect and a no- prefixed class when it is unsupported. For the CSS gradients detect, those classes are cssgradients and no-cssgradients. The CSS can keep a solid color as the baseline and layer on a gradient only when supported:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gradient with a solid fallback</title>
<script src="/assets/modernizr-custom.js"></script>
<style>
.hero {
color: #fff;
background-color: #2456a6;
padding: 2rem;
}
.cssgradients .hero {
background-image: linear-gradient(120deg, #2456a6, #482a78);
}
</style>
</head>
<body>
<main class="hero">
<h1>A usable page in either case</h1>
<p>The solid color remains if gradients are unavailable.</p>
</main>
</body>
</html>
Load the generated Modernizr script early enough for its classes to be present when the page is styled; use the loading setup appropriate to your generated build and app. If you configure a classPrefix, account for that prefix in your CSS. If you disable classes in configuration, use JavaScript properties for branching instead. Avoid using .no-cssgradients to hide necessary content: a fallback should preserve the task, not just alter appearance.
4. Use JavaScript properties for behavior
Modernizr exposes detect results as properties on the Modernizr object. Include the relevant detect in your build, then choose the enhanced or fallback behavior based on that result. Keep the fallback complete and make the choice easy to test.
// Example assumes the cssgradients detect is included in the generated build.
const panel = document.querySelector("#status-panel");
if (Modernizr.cssgradients) {
panel.classList.add("enhanced-panel");
} else {
panel.classList.add("basic-panel");
}
For a custom check, use Modernizr.addTest with a feature name and a boolean-producing expression or function. Names are lowercased on the Modernizr object. Test the behavior the app needs, rather than merely checking for a loosely related global or property. This example is deliberately schematic because the correct test depends on the required behavior:
// Replace the predicate with a check for the actual behavior your app requires.
Modernizr.addTest("apprequiredbehavior", function () {
return /* boolean result from a meaningful capability check */;
});
if (Modernizr.apprequiredbehavior) {
startEnhancedBehavior();
} else {
startFallbackBehavior();
}
Do not copy the placeholder predicate as code. Define and verify a real boolean check for your app before adding it to the build. A custom detect reports what its predicate measures; it does not polyfill the capability.
5. Handle asynchronous detects deliberately
Some tests are asynchronous. For an asynchronous feature, register a callback with Modernizr.on and make sure both outcomes have a sensible path. The callback runs once for each registration. The project recommends addTest when a custom asynchronous test needs control. Synchronous tests should be handled synchronously rather than routed through an async callback.
// Example shape for an asynchronous detect included in your build.
Modernizr.on("featureName", function (supported) {
if (supported) {
startEnhancedBehavior();
} else {
startFallbackBehavior();
}
});
Replace featureName with an asynchronous detect that exists in your build and follow that detect’s current documentation. Keep essential page content and actions usable while any asynchronous result is pending; do not make the user wait indefinitely for an optional enhancement.
6. Keep the baseline functional
Progressive enhancement works best when the page starts with content and core actions that can stand on their own. Then add visual or behavioral improvements when the required capability is present. For each detect, ask:
- Does the detect represent the exact capability the enhanced path depends on?
- Can a user still complete the main task in the fallback?
- Is the detect and fallback code worth their maintenance and payload cost?
- If a polyfill is being considered, does it actually supply the required behavior in the target browsers, and is it suitable for them?
Modernizr tells your app about detected support; it does not supply a missing implementation. A polyfill is a separate choice that must be evaluated against the actual feature and browser requirements.
7. Test both paths in real target browsers
Build a small test matrix from the browsers and devices your audience uses. For each relevant capability, verify the enhanced and fallback experiences, including the main user task. Do not infer the result from a browser’s name or version alone.
- Run the project’s current unit and integration test workflow. The repository documents
npm testand browser-served unit and integration pages; follow the installed project version’s instructions. - Open the app and test it in the browsers and devices that matter to your users.
- Verify that the expected Modernizr property and root class are present for each path.
- Exercise the actual interaction, not just the detect result. Check layout, keyboard use, content visibility, and the completion of essential actions.
- Test the fallback deliberately. Where practical, use an isolated test configuration or a controlled test fixture so the fallback is exercised even on a browser that supports the feature.
A passing detect test does not replace testing the app’s experience. Browser support changes, and the project sources do not provide a current support matrix for every detect.
8. Capture browser states for review
When reviewing a feature’s enhanced and fallback presentation, screenshots can make visual differences easier to compare. You can capture a page yourself with browser automation, or use a screenshot API when you need a repeatable image capture without managing a browser locally.
For a DIY capture, use your existing browser automation tool and set up the same URL, viewport, and state for each browser path. Modernizr itself does not take screenshots. Record the browser, viewport, and capability path alongside each image so reviewers know what they are comparing.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request can return a PNG, JPEG, WebP, or PDF capture. Cookie banners are accepted like a visitor and removed along with 60+ known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture.
For example, this cURL request saves a WebP screenshot. See the ScreenshotNeo API documentation for options such as viewport and device presets, full-page capture, element selection, dark mode, custom CSS or JavaScript, waits, request blocking, cookies, headers, geolocation, caching, and PDF settings.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
The equivalent minimal requests in Python and Node.js are:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
These calls capture a page; they do not create Modernizr’s unsupported-feature state. For a fallback comparison, configure and serve the desired app state before capture. ScreenshotNeo includes an MCP server with take_screenshot, get_page_info, and capture_pdf tools for MCP clients including Claude and Cursor.
- Cookie banners, popups, and chat widgets are removed before the shot.
- Bot checks, blank pages, and failed loads are never billed.
- An MCP server lets AI agents take screenshots.
- 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. Yearly billing gives two months free, and every feature is on every plan.
Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.
9. Performance, reliability, and cost
Performance
Build only the detects the app needs to keep the generated artifact focused. Keep the baseline useful, and avoid layering optional JavaScript or heavy fallbacks onto the core path unless they serve a real requirement. Test page behavior as well as bundle and loading behavior in the browsers you support; the research sources provide no benchmark figures.
Reliability
Use the detect that matches the feature, keep a functional fallback, and verify both in actual target browsers. Treat custom checks and asynchronous checks according to what they measure and when their results become available. Do not assume a feature detect guarantees the behavior of every third-party integration or the correctness of the overall interface.
Cost
Modernizr is distributed through an npm build workflow described by its project repository; this research does not establish package pricing. The main costs to assess in an app are engineering time, the detect artifact, fallback implementation, and ongoing browser testing. For optional ScreenshotNeo capture, the listed plans are Free for 1,000 shots/month with no card, Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free.
10. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Modernizr is undefined |
The generated script did not load, its path is wrong, or code runs before it is available. | Check the network request and script path, load the generated file, and order dependent code after it. |
| The expected feature property is missing | The detect was not selected for the custom build or the property name does not match the detect. | Include the needed detect in the build and verify its current name and API in project documentation. |
| No feature class appears on the HTML element | Classes may be disabled, the script may not have run, the detect may be absent, or classPrefix may alter the class. |
Check configuration, script loading, and the configured prefix. Use the JavaScript property if classes are intentionally disabled. |
| The unsupported browser still cannot complete the task | The fallback changes decoration but omits essential content or behavior. | Make the baseline functional first, then add the enhancement conditionally. |
| A custom test reports support incorrectly | The predicate measures a related property rather than the required behavior. | Rewrite the check around the behavior the app needs and test false and true cases in target environments. |
| An asynchronous enhancement starts too early or never appears | The code assumes a synchronous result, registers the wrong feature name, or lacks a pending/fallback state. | Use Modernizr.on for a documented async detect, confirm it is in the build, and preserve a usable state while it resolves. |
| The feature works in one browser but not another in the same family | Browser identity is not a reliable substitute for testing the particular capability and implementation. | Branch on the relevant feature and test the app in the actual target browsers. |
| The build fails on the project’s Node.js version | The selected Modernizr version may have different runtime requirements; v4 notes say Node.js 10 and below are unsupported. | Check the installed version’s requirements and use a supported Node.js environment. |
FAQ
Does Modernizr add support for a missing browser feature?
No. It reports whether a feature detect succeeds. A fallback or separately verified polyfill provides alternate behavior.
Should I detect the browser name instead?
Use capability detection for feature-dependent behavior. A browser family label does not directly answer whether the capability your code needs is available.
Do I need every Modernizr detect?
No. The project supports custom builds that select the detects needed by your app.
Where should I get Modernizr?
Follow the project’s npm build instructions and verify the current package workflow. The repository warns that its website is outdated and broken.
Can I use screenshots to verify the fallback?
Yes, if you first make the app render the intended fallback state and keep the browser, viewport, and state consistent across captures.


