Browser Support Strategy
Decide what to support before you argue about prefixes in code review.
Apply
Policy belongs in your team’s written strategy. This page covers the engineering side: how to express targets with browserslist, how tools consume them, and how to layer progressive CSS without turning every page into a compatibility puzzle.
For product-level decisions and common mistakes, readBrowser Support Strategyfirst—then wire the targets below into your build.
Browserslistis a shared query language for “which browsers does this project run on?” PostCSS (Autoprefixer), Lightning CSS, SWC/Babel, and other tools read the same configuration so prefixes and transpilation stay aligned with what you actually support.
Put queries in a .browserslistrc at the repository root, or inpackage.json under a browserslist key. Avoid duplicating different targets per package in a monorepo unless you intend different CSS output per app—shared design tokens and CSS bundles should usually share one resolved list.
# https://github.com/browserslist/browserslist
# One shared query list for Autoprefixer, Lightning CSS, @babel/preset-env, etc.
last 2 versions
> 0.5%
not deadQueries are data, not superstition: combine coverage stats (> 0.5%), recency (last 2 versions), and explicit exclusions (not dead) with what your analytics and contracts require—or use feature queries such assupports css-grid when yoursupport matrix is capability-first rather than version-first. Run the command below whenever you change the file.
pnpm exec browserslistOnce browserslist is set, keep these behaviours consistent:
More detail on PostCSS plugins, Stylelint, and monorepo caveats lives underTooling.
Custom media(@custom-media) lets you name breakpoints and preference queries once—usually in the settings layer—and reference them in component CSS as @media (--desktop) instead of repeating magic numbers. Directional names such as --from-tablet and accessibility queries such as --reduced-motion can sit beside layout breakpoints in the same file.
No major browser implements @custom-media yet.What ships in production is ordinary @media after your build expands the aliases (PostCSS, Lightning CSS, or equivalent). You cannot paste custom-media syntax into a stylesheet that reaches the browser without that transform—unlike cascade layers or :is(), there is no progressive runtime fallback.
/* settings layer — define once */
@custom-media --desktop (width >= 1280px);
@custom-media --reduced-motion (prefers-reduced-motion: reduce);
/* component CSS — readable at authoring time */
.product-card {
@media (--desktop) {
grid-template-columns: repeat(3, 1fr);
}
@media (--reduced-motion) {
transition: none;
}
}
/* build output — what the browser actually parses */
/* @media (width >= 1280px) { … } */
/* @media (prefers-reduced-motion: reduce) { … } */The tradeoff is explicit: every path that produces CSS must run the expand step—local dev, CI, preview, and any CMS or micro-frontend that might inject styles. If one pipeline skips the build while the rest of the repo uses @media (--tablet), that CSS will not parse.
For a small site with one author and few breakpoints, a tool only for custom media may not pay for itself. A single shared constants file, comments, or careful discipline can be enough.
For larger codebases—many contributors, long-lived products, or a shared design system—the step is usually worth it:
--desktop stays aligned.@media (--until-desktop) states intent; scattered1279px does not.prefers-reduced-motion like a layout breakpoint instead of bolting one-off media blocks into random files.Wire custom media into the same browserslist-backed pipeline as prefixes and other syntax lowering so expanded queries match the browsers you actually support. Examples and anti-patterns for naming breakpoints live under Tooling → Custom media queries.
@supports and progressive enhancementBrowserslist answers “what do we transform or prefix?”. In authored CSS,@supports answers “does this runtime understand this capability?” Use it for meaningful fallbacks—layout modes, selectors, units—not for sprinkling forks around every new property.
@supports (display: grid) {
.product-grid {
display: grid;
gap: var(--space-m);
}
}
@supports not (display: grid) {
.product-grid {
display: flex;
flex-wrap: wrap;
gap: var(--space-m);
}
}Prefer simpler baselines first: solid layout and readable typography without advanced features, then enhance where support exists. That matches how LSCSS uses layers: keep the stable story in lower layers and isolate experiments.
LSCSS examples and site CSS assume a reasonably current evergreen baseline. Your exact floor is a business decision; these are the capabilities you will see referenced most often in Apply and Learn:
@layer) for predictable override order@custom-media) for named breakpoints— authored in CSS, delivered as plain @media via the build; see Custom media queries and the build step:is(), :where(), :not(), and:has() where examples stay concise.:has() has a newer support curve than:is(), :where(), or :not()—confirm it against your resolved browserslist before critical UI depends on it.@scope where discussed explicitly—know your target support before relying on it in productionIf you must support older engines, document which features are authored behind build transforms, which are gated with @supports (for example@supports selector(:has(*)) for relational selectors), and which are simply off the table until the baseline moves.
Browserslist tells the build what to transform; a written matrix tells people what you promise users. Keep it next to the code (for exampledocs/browser-support.md or your team handbook), link it from onboarding, and update it when the baseline changes—not only at project kickoff.
Use three tiers so “supported” is not argued from memory in every review:
You do not have to anchor policy on version numbers. Two approaches are common; pick one as primary and add the other when it helps.
| Approach | What you document | Best when |
|---|---|---|
| Feature baseline | Capabilities the product requires—CSS, JavaScript, and platform APIs—and how you deliver or fall back (native, build, polyfill, transpile) | Progressive enhancement, polyfill/transpile strategies, or when “Chrome 120” is less useful than “fetch required” or “container queries required” |
| Version / environment list | Named browsers, minimum versions, webviews, and tiers | Contracts, QA device labs, analytics-driven floors, or embedded shells you must name explicitly |
The same idea applies outside CSS. A feature baseline forJavaScript might list fetch, ES modules,dynamic import(), or observers your app relies on—with delivery marked as native, transpiled, or polyfilled—instead of debating “which Safari version” in every ticket. You stop guessing what “we support last two versions” means for a given API and document what must work, what degrades, and what is off the table.
Many teams treat feature tables as source of truth for engineering (CSS plus JS where relevant) and keep a version list as an optional appendix for QA and stakeholders. For CSS tooling, browserslist can follow coverage queries such as last 2 versions,feature queries such as supports css-grid, or a mix—see thebrowserslist docs. For JS, the same browserslist config often feeds Babel or SWC; align those transforms with the capabilities in your matrix. Runtime checks in CSS use@supports (see @supportsabove); in JS use feature detection or conditional loading, not version sniffing.
Start with the CSS rows below. Extend the same columns for JavaScript and platform APIs in your copied template (see the Markdown block)—policy, delivery, and fallback—so one document covers the stack.
For each capability, record policy (required everywhere you claim fully supported, progressive with a fallback, or not used),delivery (native, build transform, polyfill, or@supports in CSS), and what happens when the feature is missing.
| Capability | Policy | Delivery | Fallback when missing | Notes |
|---|---|---|---|---|
Cascade layers (@layer) | — | — | — | — |
| Design tokens | — | — | — | — |
| Custom media | — | — | — | Build must expand in every CSS pipeline |
| Container queries | — | — | — | — |
| Nesting | — | — | — | — |
:is() / :where() / :not() | — | — | — | — |
:has() | — | — | — | Often progressive; gate with @supports selector(:has(*)) |
@scope | — | — | — | — |
| Logical properties | — | — | — | — |
Policy examples: Required,Progressive, Not used.Delivery examples: Native, Build,@supports.
Use this table when you need to name engines for legal text, device labs, or analytics—not as a substitute for describing what CSS must do. Add rows for webviews, kiosk shells, or partner embeds that matter to your product.
| Browser / environment | Minimum version | Tier | Notes |
|---|---|---|---|
| Chrome (desktop) | — | — | — |
| Firefox | — | — | — |
| Safari (macOS) | — | — | — |
| Microsoft Edge | — | — | — |
| Safari (iOS) | — | — | — |
| Chrome (Android) | — | — | — |
| Samsung Internet | — | — | — |
| Embedded webview | — | — | Name the product (in-app browser, partner shell, etc.) |
Copy the Markdown below into your repository and replace the placeholders. It includes an optional JavaScript / platform API section using the same pattern. State whether your primary baseline is feature-, version-, or both-style in the policy header. Attach resolved browserslist output to release notes when the baseline changes.
# Browser support matrix
<!-- Copy into your repo (e.g. docs/browser-support.md). Update when policy or browserslist changes. -->
## Policy
| Field | Value |
| --- | --- |
| Last updated | YYYY-MM-DD |
| Owners | |
| Primary baseline | Feature / Version / Both |
| Browserslist | See .browserslistrc (queries and/or feature queries) |
| Next review | |
## Support tiers
- Fully supported — critical journeys meet the baseline; defects are release blockers.
- Best effort — core tasks work; minor visual differences acceptable.
- Not supported — no test commitment.
## Two ways to document the baseline
Use a feature baseline when capability matters more than a version number (CSS, JS, and platform APIs).
Use a version list when contracts, QA devices, or analytics name specific engines.
Many teams treat the feature tables as source of truth and keep versions as an optional appendix.
## Feature baseline (primary)
Policy: Required = must work everywhere you claim fully supported.
Progressive = enhanced path + documented fallback. Not used = off the table.
| Capability | Policy | Delivery | Fallback when missing | Notes |
| --- | --- | --- | --- | --- |
| Cascade layers (@layer) | | Native / Build / Not used | | |
| Design tokens (custom properties) | | Native | | |
| Custom media (@custom-media) | | Build | Plain @media or simpler layout | Every pipeline must expand |
| Container queries | | Native / Not used | | |
| Nesting | | Native / Build | Flat selectors | |
| :is(), :where(), :not() | | Native | Longer selectors | |
| :has() | | Native / @supports | Simpler DOM or JS hook | |
| @scope | | Native / Not used | Layers + naming discipline | |
| Logical properties | | Native | Physical properties | |
## JavaScript and platform APIs (optional, same pattern)
| Capability | Policy | Delivery | Fallback when missing | Notes |
| --- | --- | --- | --- | --- |
| ES modules | | Native / Transpile | | |
| fetch | | Native / Polyfill | | |
| dynamic import() | | Native / Transpile / Not used | | |
| IntersectionObserver | | Native / Polyfill | | |
| (add rows your app needs) | | | | |
## Browser versions (optional appendix)
| Browser / environment | Minimum version | Tier | Notes |
| --- | --- | --- | --- |
| Chrome (desktop) | | | |
| Firefox | | | |
| Safari (macOS) | | | |
| Microsoft Edge | | | |
| Safari (iOS) | | | |
| Chrome (Android) | | | |
| Samsung Internet | | | |
| Embedded webview (name product) | | | |@supports fallbacks.@custom-media, confirm every CSS delivery path (dev, CI, preview, embedded bundles) runs the expand step—no raw@media (--name) in shipped CSS.