postcss-extend-rule
A PostCSS plugin that brings Sass-style @extend at-rules and % placeholder selectors to plain CSS.
Repository Health
Technical Analysis
postcss-extend-rule is a PostCSS plugin that implements the speculative CSS Extend Rules Specification, letting authors write @extend selector; inside a rule to pull in the declarations of another matching selector, and define reusable %placeholder blocks that only exist to be extended and are stripped from the final output. Under the hood it walks the PostCSS AST for @extend at-rules, finds rules whose selector matches the referenced identifier, clones their nodes into a @nest-style rule scoped to the extending selector, and re-nests the result through postcss-nesting so the generated CSS stays valid and correctly ordered relative to media queries and other at-rules.
The plugin exposes three configurable behaviors — onFunctionalSelector, onRecursiveExtend, and onUnusedExtend — each accepting remove (default), ignore, warn, or throw, giving teams control over how strictly malformed or dead @extend usage is treated during a build. This makes it usable both as a lenient drop-in for prototyping and as a strict linter-like gate in CI, where an unused placeholder or a functional selector that leaked into shipped CSS can be turned into a hard build failure instead of silently passing through.
Because it operates purely as a PostCSS transform with no runtime footprint, it fits into any PostCSS-based pipeline (postcss-cli, Webpack, Create React App via config overrides, Gulp, Grunt) without adding weight to the shipped stylesheet — all @extend resolution happens at build time, and the output is standard CSS with no proprietary at-rules left behind.
What You Get
@extend selector;support inside any rule, pulling in the extended rule’s declarations without duplicating source%placeholderfunctional selectors that define extend-only styles and are automatically stripped from output- Configurable
nameoption to rename the at-rule keyword away from the defaultextend - Three-way policy control (
remove/ignore/warn/throw) for functional selectors, recursive extends, and unused extends - Automatic re-nesting via
postcss-nestingso extended rules stay correctly scoped inside media queries and other at-rules - Zero runtime cost — all resolution happens during the PostCSS build step, shipping plain CSS
Common Use Cases
- Sharing a base set of declarations (like a button or card style) across multiple selectors without hand-duplicating properties
- Defining
%placeholderblocks for styles that should only ever be reached via@extend, keeping them out of the final stylesheet - Enforcing CI failures on unused or malformed
@extendusage by settingonUnusedExtend/onFunctionalSelectortothrow - Migrating Sass
@extend-heavy stylesheets to a plain-CSS PostCSS pipeline while preserving the same authoring pattern - Combining with other csstools PostCSS plugins in a
postcss-preset-env-style pipeline for future-CSS authoring today
Under The Hood
Architecture
The entire transform lives in a single OnceExit PostCSS visitor in src/index.js: it first walks the AST for at-rules matching the configured extend keyword, resolves each one against a selector-identifier regex built from the at-rule’s params, and replaces matches by cloning the target rule’s declarations into a synthetic @nest at-rule that preserves the original nesting chain of parent rules and at-rules; a second walk then removes or reports any leftover %placeholder functional selectors. A WeakMap guards against re-processing an at-rule that was already resolved, which is also how recursive/self-referential extends are detected and handled per the configured policy. Because the nested output has to be flattened back into valid CSS, the plugin clones the parent chain into a throwaway PostCSS root and re-runs postcss-nesting synchronously on it before splicing the result back in — a clear, if unconventional, choice to reuse an existing de-nesting implementation rather than hand-roll selector-combination logic.
Tech Stack
The package is plain, dependency-light JavaScript with a single runtime dependency (postcss-nesting) and postcss itself as a peer dependency pinned to the 8.x line. It’s built with Rollup (outputting both dist/index.cjs and dist/index.mjs for CommonJS/ESM consumers) via a minimal rollup.mjs config using @rollup/plugin-babel, and targets Node 12/14/16+ per its engines field. There’s no TypeScript, no bundler config beyond Rollup, and no external service integration — it’s a self-contained AST transform.
Code Quality
Tests run via Node’s built-in node --test runner against the @csstools/postcss-tape fixture harness, with each case defined as a .css input and a matching .expect.css file under test/ (dozens of scenarios covering nesting, media queries, functional selectors, and several numbered GitHub issue regressions), plus a REWRITE_EXPECTS mode for regenerating fixtures. Linting is enforced via ESLint with eslint:recommended and explicit style rules (tabs, single quotes, always-semi), and pretest wires build before test so fixtures always run against freshly compiled output; CI runs the same npm test via a GitHub Actions workflow on every push. There’s no static type system, but the small single-file surface area and extensive fixture coverage keep the practical risk low.
API Design
The public API is a single default-exported plugin factory (postcssExtendRule(options)) following the standard PostCSS plugin contract, so integrating it is one line in any existing postcss([...]) pipeline. Its three behavior-control options share one consistent four-value vocabulary (remove/ignore/warn/throw), which keeps the mental model simple even though there are three independent axes to configure, and sensible defaults mean most consumers never need to touch the options object at all.