How to Use CSS Feature Detection for Cross-Browser Compatibility
Use CSS @supports and JavaScript CSS.supports() to add browser-aware enhancements while keeping a usable fallback—and learn what feature detection cannot guarantee.
Use CSS @supports to apply an enhancement only when a browser accepts the exact CSS capability it needs. First write a usable baseline; then add enhanced declarations inside the feature query. If JavaScript needs to make a CSS capability decision, use CSS.supports(). Neither method proves that a browser’s implementation is complete or bug-free, so check compatibility for the exact feature and test the result in target browsers.
This guide answers How to Use CSS Feature Detection for Cross-Browser Compatibility with a runnable example, condition syntax, JavaScript detection, testing guidance, and fixes for common problems.
1. Start with a working baseline
Write the page so its content remains usable without the enhancement. Put the enhanced rules after the baseline and wrap them in an @supports condition that tests the property and value your enhancement depends on:
/* styles.css */
.cards {
display: block;
}
.cards > * + * {
margin-block-start: 1rem;
}
@supports (display: grid) {
.cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
}
.cards > * + * {
margin-block-start: 0;
}
}
If Grid is not supported, the browser uses the block layout and spacing. If it accepts display: grid, the enhanced declarations apply and the baseline sibling spacing is removed. Keep the fallback useful: the goal is a readable, functional page even when an enhancement is unavailable. See MDN’s feature queries guide and @supports reference.
2. Write precise feature-query conditions
A declaration condition has the form @supports (property: value). The browser evaluates whether it understands the tested declaration. Use the exact value or selector that matters to the enhancement rather than treating a related capability as proof of support for every detail.
/* One declaration */
@supports (display: grid) {
.layout { display: grid; }
}
/* Every condition must pass */
@supports (display: grid) and (gap: 1rem) {
.layout {
display: grid;
gap: 1rem;
}
}
/* Either condition can pass */
@supports (display: grid) or (display: flex) {
.layout { display: grid; }
}
/* Target a selector capability */
@supports selector(:has(a)) {
.card:has(a) { outline: 1px solid currentColor; }
}
Combine conditions with and, or, and not. Parenthesize declaration tests. Selector conditions use selector(), which is useful when the enhancement depends on selector syntax such as :has(). A passing condition indicates the tested syntax is accepted; it does not independently verify every declaration in the block or the rendered behavior. See MDN’s condition syntax reference.
When to use @supports not
Use @supports not (...) when an explicit alternative is needed for a capability the browser lacks:
.panel {
display: block;
}
@supports not (display: grid) {
.panel > * + * { margin-block-start: 1rem; }
}
@supports (display: grid) {
.panel {
display: grid;
gap: 1rem;
}
.panel > * + * { margin-block-start: 0; }
}
Often you can avoid the negative branch: declare the fallback normally and add only the enhancement inside a positive query. This keeps the cascade simpler and reduces duplicated rules.
When a feature query is unnecessary
Browsers ignore declarations they do not recognize. If a new declaration can simply be added without disrupting the baseline, ordinary CSS may be enough. Use @supports when several enhanced rules belong together, when the fallback must change, or when a clear conditional boundary helps explain the design.
3. Use CSS.supports() when JavaScript needs the answer
For CSS-only styling, prefer @supports so the browser applies the conditional styles directly. JavaScript detection is useful when script itself must choose behavior based on a CSS capability—for example, deciding whether to load or activate code that depends on a particular layout feature.
if (window.CSS && CSS.supports("grid-template-columns", "subgrid")) {
// Activate behavior that depends on subgrid.
document.documentElement.classList.add("has-subgrid");
}
CSS.supports(property, value) returns a boolean. It can also take a supports-condition string:
const supportsLayout = window.CSS && CSS.supports(
"(display: grid) and (gap: 1rem)"
);
if (supportsLayout) {
// Choose a CSS-dependent script path.
}
The window.CSS guard allows the code to run in environments where the API itself is unavailable. Use the boolean to select a meaningful fallback; do not use it as a proxy for browser identity.
4. Separate syntax support, compatibility, and actual behavior
Feature detection answers whether the browser considers the tested syntax valid. It does not detect partial implementations, browser bugs, or whether an interaction looks and behaves correctly in the page. Cross-browser confidence comes from using three checks together:
- Feature query: Does this browser accept the declaration or selector condition?
- Compatibility information: Which versions are reported to support this exact feature? Check the compatibility information on the relevant MDN feature page.
- Browser testing: Does the rendered page work as intended in the environments your users rely on?
Avoid user-agent sniffing as a substitute for capability checks. Browser-name branches assume identity predicts support; a feature query adapts to the capability. For behavior-sensitive features, consult current compatibility data and test affected versions. MDN’s cross-browser testing introduction and Playwright’s browser documentation describe testing across browser engines.
5. A practical implementation and verification checklist
- Identify the specific CSS declaration or selector your enhancement needs.
- Write semantic content and baseline styles that remain usable without it.
- Add the enhancement inside an
@supportscondition for that exact capability. - Use
andwhen multiple capabilities are all required; useoronly when either is an acceptable path. - Check compatibility data for the exact feature and target browser versions.
- Test both the fallback and enhanced rendering in the browsers and devices that matter.
- If the visual or functional result can fail despite a passing query, test the behavior directly rather than adding more syntax checks.
For a repeatable browser matrix, a Playwright test can open your page in each configured project and assert meaningful behavior or layout outcomes. A syntax check alone is not enough: verify content remains visible, controls remain usable, and the enhanced layout does not introduce overflow at relevant viewport sizes. Refer to the official Playwright browser guide for browser setup.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The enhanced rules never apply. | The condition is invalid or tests a different property/value than the rule needs. | Check parentheses and spelling. Test the exact value with CSS.supports() in the affected browser and confirm the query matches the enhancement. |
| The query passes, but the page still looks wrong. | The browser accepts the syntax but has an incomplete implementation, a bug, or a conflict in the cascade. | Inspect computed styles, check compatibility notes for the exact feature, and reproduce in the affected browser. Add a targeted fallback or adjust the enhancement. |
| The fallback is missing or unusable. | The baseline depends on the same unsupported feature, or the enhancement overwrites necessary spacing or sizing. | Make the default rules independently usable. Check the page with enhanced declarations disabled. |
| Some details break even though a related feature is supported. | The query checks a broad or adjacent capability instead of the precise value or selector needed. | Test the exact declaration/value or selector condition. Do not infer support for one feature from another. |
| JavaScript throws because CSS is undefined. | The script environment does not expose the CSS support API. | Guard the call with window.CSS && and choose the safe fallback when unavailable. |
| One browser passes while another fails. | Support differs by browser version, or their implementations behave differently. | Check current feature compatibility data and run the page in the affected browser engines and versions. |
7. Performance, reliability, and cost
CSS feature queries are browser-native styling conditions and do not require a remote detection service. Keep the conditional CSS focused and avoid maintaining duplicated branches when the normal cascade can provide the fallback. JavaScript detection adds a decision point; use it only when script needs the capability result.
Reliability depends on the fallback and on validating actual behavior. A positive query is not a compatibility guarantee. Browser testing costs time and, depending on the environment or service chosen, may involve infrastructure or service costs; this guide makes no claim about a particular testing service’s pricing or performance. Select the environments based on your users and check current compatibility data as browser releases change.
Or skip the browser setup
If your workflow also needs website screenshots for visual review or documentation, ScreenshotNeo offers a one-request screenshot API and an MCP server for AI agents. It is a separate way to capture a page, not a replacement for validating CSS behavior across browsers.
One GET request returns an image or PDF. Example using cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Read the ScreenshotNeo API documentation for request options. ScreenshotNeo accepts cookie and consent banners as a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, 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 including take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month, with no card.
FAQ
Does @supports guarantee cross-browser compatibility?
No. It checks whether the browser accepts the tested condition, not whether its implementation is complete or bug-free.
Should I use @supports or CSS.supports()?
Use @supports for conditional styling. Use CSS.supports() when JavaScript must branch on CSS capability.
Do I need a feature query for every new CSS property?
No. Unrecognized declarations are ignored. A query is most useful when the enhancement needs a grouped boundary or a different fallback.
Can I detect a browser bug with @supports?
Not reliably. Check feature-specific compatibility notes and test the affected behavior in the browser versions you support.


