Apply

Browser support in LSCSS projects

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.

Browserslist

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.

BASH code example
# https://github.com/browserslist/browserslist
# One shared query list for Autoprefixer, Lightning CSS, @babel/preset-env, etc.

last 2 versions
> 0.5%
not dead

Queries 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.

Print resolved browsers
pnpm exec browserslist

How the build uses the same targets

Once browserslist is set, keep these behaviours consistent:

  • Vendor prefixes.Autoprefixer (or Lightning CSS with the right options) uses the list to decide which prefixes to emit. If the list is stale, you either ship unnecessary prefixes or miss real devices.
  • Syntax lowering.Some pipelines transpile nesting, custom media, or other syntax. Those steps should assume the same browser surface as your prefix step.
  • CI.Run production builds in CI with the same environment variables and config paths as local machines so browserslist resolution does not drift.

More detail on PostCSS plugins, Stylelint, and monorepo caveats lives underTooling.

Custom media queries and the build step

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.

CSS code example
/* 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.

When the build step is worth it

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:

  • One source of truth.Change a breakpoint once; every component that references--desktop stays aligned.
  • Readable reviews.@media (--until-desktop) states intent; scattered1279px does not.
  • Fewer silent drifts.Without named media, “the tablet breakpoint” slowly becomes three different numbers in three folders.
  • Preference queries as first-class.Treat 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 enhancement

Browserslist 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.

CSS code example
@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.

Modern features this methodology leans on

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:

  • Cascade layers (@layer) for predictable override order
  • Custom properties for tokens and theme boundaries
  • Logical properties and values for direction-aware layout
  • Custom media (@custom-media) for named breakpoints— authored in CSS, delivered as plain @media via the build; see Custom media queries and the build step
  • Container queries and nesting for responsive, readable component CSS
  • Selector features: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 production

If 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.

Support matrix template

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:

  • Fully supported — critical journeys meet your documented baseline; defects are release blockers.
  • Best effort — core tasks work; minor visual differences are acceptable.
  • Not supported — no test commitment; degradation or blocking is acceptable.

Two ways to document the baseline

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.

ApproachWhat you documentBest when
Feature baselineCapabilities 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 listNamed browsers, minimum versions, webviews, and tiersContracts, 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.

Feature baseline (primary template)

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.

CapabilityPolicyDeliveryFallback when missingNotes
Cascade layers (@layer)
Design tokens
Custom mediaBuild 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.

Browser versions (optional appendix)

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 / environmentMinimum versionTierNotes
Chrome (desktop)
Firefox
Safari (macOS)
Microsoft Edge
Safari (iOS)
Chrome (Android)
Samsung Internet
Embedded webviewName 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.

Diagram
# 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) | | | |

Practical checklist

  • Publish a written support matrix using thetemplate above—feature baseline, version list, or both—and keep it aligned with browserslist and@supports fallbacks.
  • Commit browserslist queries next to the code that consumes them.
  • Run the browserslist command above after changes and store the output in release notes when support shifts.
  • If you use @custom-media, confirm every CSS delivery path (dev, CI, preview, embedded bundles) runs the expand step—no raw@media (--name) in shipped CSS.
  • Test critical journeys in at least one device class per major engine you claim.
  • Revisit targets when analytics, regulation, or embedded webviews change.

Next useful pages