Using PostCSS Preset‑Env to Polyfill Modern CSS for Targeted Browsers
Learn how to install and configure PostCSS Preset‑Env to automatically polyfill modern CSS based on a browserslist query, with a worked example and common pitfalls.
20 May 2026, 03:35 UTC

Quick answer
Install PostCSS and the preset‑env plugin, add a browserslist query, and run the CLI to get CSS that includes only the polyfills needed for your target browsers.
How the plugin works
PostCSS processes your source stylesheet through a chain of plugins. postcss-preset-env reads the browserslist configuration (either in package.json or a .browserslistrc file) and enables the set of features that are safe for those browsers. For each enabled feature it adds the necessary transforms – vendor prefixes, rewritten pseudo‑classes, remapped logical properties, etc. – while leaving unsupported cutting‑edge syntax untouched unless you enable experimental flags.
Worked example
- Initialize a project (if you don’t have one) and install the dependencies:
npm init -y
npm i -D postcss postcss-preset-env
- Create a
browserslistentry inpackage.json:
{
"browserslist": ["> 0.2%", "not dead"]
}
- Add a PostCSS configuration file:
// postcss.config.js
module.exports = {
plugins: [
require('postcss-preset-env')({
stage: 2,
// browserslist: ['IE 11', 'last 2 versions'] // optional override
})
]
};
- Write some modern CSS in
src/styles.css:
/* src/styles.css */
:root {
--accent: color(display-p3 0.6 0.2 0.8);
}
@custom-media --wide-view (width >= 800px);
.card {
background: var(--accent);
padding: 1rem;
border-radius: 0.5rem;
/* nesting */
& h2 {
margin: 0;
}
&:not(.disabled) {
cursor: pointer;
}
}
@media (--wide-view) {
.card {
max-width: 600px;
}
}
- Run the PostCSS CLI to produce the output:
npx postcss src/styles.css -o dist/styles.css --config postcss.config.js
Inspect dist/styles.css. You should see:
- The
color()function rewritten to an RGB fallback (or left as‑is if the target browsers support display‑p3). - The custom media query replaced with the raw
@media (width >= 800px)rule. - Nested rules flattened to selectors like
.card h2. - The
:not(.disabled)pseudo‑class prefixed for browsers that need it (e.g.,:not(.disabled)stays the same, but if targeting IE 11 you might see:not(.disabled)unchanged because IE 11 supports it, while other transforms may add prefixes). - Vendor prefixes added to properties that need them based on the browserslist query.
Limits and common mistakes
Limits
- Some very new features (e.g., container queries, @property) are not covered by preset‑env unless you enable experimental flags or add extra plugins such as
postcss-container-queries. - If the browserslist query is too broad (e.g.,
defaultsor"> 0%"), the plugin may apply polyfills for browsers you don’t actually support, increasing file size. - Misplacing the
browserslistconfiguration (forgetting to add it topackage.jsonor a dedicated file) causes the plugin to fall back to its own defaults, which may be unexpected.
Common mistakes
- Installing only
postcss-preset-envwithout the corepostcsspackage; the CLI will ignore the plugin and output unchanged CSS. - Using the plugin in a build tool (Webpack, Rollup, etc.) but forgetting to pass the
optionsobject, resulting in the default stage 0 behavior. - Assuming that enabling
stage: 3automatically polyfills all stage 3 features; some still require explicit flags.
Verification
- Run the command shown above.
- Open the generated
dist/styles.cssand look for the transformed rules described in the “Worked example” section. - Optionally, run
npx browserslistto see the exact browser list that PostCSS is using; compare it with the transforms you observe.
If the output still contains the original modern syntax (e.g., the color() function or nested rules) and you expect it to be transformed, double‑check that the postcss.config.js file is being read (the CLI prints a warning if it cannot find it) and that the browserslist query matches your intentions.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.