postcss-extend-rule

A PostCSS plugin that brings Sass-style @extend at-rules and % placeholder selectors to plain CSS.

Library
npm
v4.0.0
54stars
CC0-1.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
48/100Fair
Development Activity64
Maintenance20
Community36
Maturity60
Momentum12

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
67/100Good
Architecture68
Code Quality70
Innovation55
Learning Curve75

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
  • %placeholder functional selectors that define extend-only styles and are automatically stripped from output
  • Configurable name option to rename the at-rule keyword away from the default extend
  • Three-way policy control (remove/ignore/warn/throw) for functional selectors, recursive extends, and unused extends
  • Automatic re-nesting via postcss-nesting so 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 %placeholder blocks for styles that should only ever be reached via @extend, keeping them out of the final stylesheet
  • Enforcing CI failures on unused or malformed @extend usage by setting onUnusedExtend/onFunctionalSelector to throw
  • 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.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search