# CSS Post Processor vs Sass, LESS, PostCSS and native CSS

> How CSS Post Processor compares with Sass, LESS, PostCSS and modern native CSS in 2026 - build step, runtime features, selectors, variables and when to use each.

Source: https://csspp.dev/compare/


Sass, LESS and PostCSS transform CSS **before** it reaches the browser. CSS Post Processor transforms it **in** the browser, where it can see the page. The two approaches aren't competing for the same job, and the table below shows where each one fits.

## At a glance

| | CSS Post Processor | Native CSS (2026) | Sass / SCSS | LESS | PostCSS |
|---|---|---|---|---|---|
| Runs | In the browser | In the browser | At build time | At build time (or browser) | At build time |
| Build step needed | <span class="yes">No</span> | <span class="yes">No</span> | <span class="no">Yes</span> | <span class="no">Usually</span> | <span class="no">Yes</span> |
| Variables | Yes, plus JavaScript expressions | Custom properties | Yes | Yes | Via plugins |
| Variables inside `url()` | <span class="yes">Yes</span> | <span class="no">No</span> | Yes (static only) | Yes (static only) | Via plugins |
| Nesting | Yes | Yes | Yes | Yes | Via plugins |
| Mixins with parameters | Yes | No | Yes | Yes | Via plugins |
| Values from the live page (viewport, element sizes) | <span class="yes">Yes</span> | Partly (`vw`, container units) | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> |
| Per-element values in any property | <span class="yes">`$[index]`</span> | `sibling-index()`, numbers only | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> |
| Seen / in view selector | <span class="yes">`:seen`, `:in-view`</span> | <span class="no">No</span> (Chromium-only triggers) | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> |
| Sticky "stuck" selector | <span class="yes">`:stuck`</span> | Chromium only | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> |
| Overflow / truncation selector | <span class="yes">`:overflowing`</span> | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> |
| Select by text | <span class="yes">`:contains()`</span> | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> |
| Numeric attribute comparison | <span class="yes">`[data-x<5]`</span> | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> |
| Active link selector | <span class="yes">`:current`</span> | <span class="no">No</span> (`:local-link` unimplemented) | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> |
| Scroll position and direction | <span class="yes">`:scrolled`, `:scrolling-down`</span> | Chromium only | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> |
| Image loaded / broken state | <span class="yes">`:loaded`, `:broken`</span> | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> |
| Flex-wrap detection | <span class="yes">`:wrapped`</span> | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> |
| Fit text, equal heights | <span class="yes">`fit()`, `same`</span> | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> |
| Pseudo-element clicks, hover, styling from JS | <span class="yes">`css.on()`, `::after:hover`, `css.pseudo()`</span> | <span class="no">No</span> (Chrome-only `element.pseudo()`, no events) | <span class="no">No</span> | <span class="no">No</span> | <span class="no">No</span> |
| Parent selection | `:has()` plus the selectors above | `:has()` | `:has()` | `:has()` | `:has()` |
| Loops, modules, functions | JavaScript expressions | Limited | Yes | Partial | Via plugins |
| Size added to the page | 10.4 KB | 0 | 0 | 0 | 0 |

## File size: with and without css-pp {#file-size}

This site is styled with css-pp itself. Its stylesheet uses nesting, variables and mixins, and your browser compiled it a moment ago. Here is how big the same stylesheets are when written with css-pp and as the plain CSS you would have to write without it:

| Stylesheet | Written with css-pp | Same CSS without it | Less to write |
|---|---|---|---|
| This website | 17.8 KB | 19.7 KB | 10% |
| A production theme with heavy nesting and mixins | 60.4 KB | 77.9 KB | 22% |

Sizes are minified, with comments removed. You can compare the two files for this site yourself: [`/assets/site.css`](/assets/site.css) is the css-pp source your browser processes, and [`/assets/site.plain.css`](/assets/site.plain.css) is the same stylesheet compiled at build time. Browsers without JavaScript get the plain one.

The difference is in what you maintain. In this site's stylesheet, the border style `1px solid var(--border)` is written once and appears 28 times in the output, so changing it is a one-line edit. Selectors are written once per component instead of once per rule, and the output has 227 rules.

What css-pp does **not** do is save bandwidth. Gzip removes repetition too, so the compressed files end up almost the same size (5.0 KB and 5.0 KB for this site), and the library itself adds 10.4 KB (cached after the first page). Choose it for shorter, easier-to-change stylesheets and for the runtime features plain CSS lacks, not for smaller downloads.

## vs native CSS

Modern CSS has caught up with much of what preprocessors used to offer. Custom properties, nesting, `:has()`, container queries, `clamp()`, `aspect-ratio` and, since 2026, `sibling-index()` all work in every current browser. **If native CSS does what you need, use native CSS.**

What it still can't do, and what css-pp adds:

- **Know what the user has seen.** There is no selector for "has been scrolled into view". Scroll-triggered animations exist only in Chromium and only drive animations, not arbitrary styles.
- **Know that a sticky element is stuck.** `scroll-state(stuck)` queries are Chromium-only.
- **Know that content overflows.** There is no native truncation selector. The CSS Working Group discussion is ongoing.
- **Read text or compare numbers.** `:contains()` was dropped from the CSS3 drafts and never shipped, and attribute selectors only compare text.
- **Build URLs from values.** `url()` doesn't accept variables or functions, so image sequences need one rule per image.

## vs Sass and LESS

Sass and LESS are excellent at what they do: modules, loops, functions and build-time variables, compiled once into static CSS. They run before the page exists, so they can't use anything that depends on it: how many slides there are, how wide the window is, whether a header is stuck, what text a badge contains.

css-pp needs no Node.js toolchain, which matters for sites without a build pipeline: plain HTML, WordPress themes, server-rendered PHP or Perl, or a CMS where you can only edit a stylesheet.

**Using both:** you can compile Sass to CSS and still open a separate, smaller stylesheet with css-pp for runtime features. Don't run css-pp syntax through the Sass compiler; both use `$` for variables.

## vs PostCSS

PostCSS is a build-time toolkit: autoprefixing, future-syntax polyfills, minification and hundreds of plugins. css-pp is not a PostCSS replacement, and you can use both: PostCSS for your main build, css-pp for the runtime features that need the live page.

## vs JavaScript libraries

Many css-pp features replace a small library or a hand-written script:

| Instead of | Write |
|---|---|
| Scroll-reveal libraries, or a custom `IntersectionObserver` that adds classes | `.card:not(:seen) { opacity: 0 }` |
| A scroll listener that toggles a "stuck" class | `header:stuck { box-shadow: ... }` |
| JavaScript that compares `scrollWidth` to `clientWidth` | `.title:overflowing { ... }` |
| jQuery `:contains()` plus class toggling | `.badge:contains("Sold out") { ... }` |
| A loop that sets a background image on each slide | `url(/img/slide-$[index].jpg)` |
| A script that adds an `active` class to the current nav link | `nav a:current { ... }` |
| Hide-on-scroll header libraries | `html:scrolling-down header { translate: 0 -100% }` |
| Bootstrap scrollspy | `.toc a:target-in-view { ... }` |
| FitText, fitty | `h1 { font-size: fit(24px, 120px) }` |
| matchHeight | `.card h3 { height: same }` |
| Guessing whether a click landed on an `::after` from its coordinates | `css.on('click', '.chip::after', fn)` |
| `onload` / `onerror` handlers on images | `img:not(:loaded) { opacity: 0 }`, `img:broken { ... }` |

The CSS version is shorter, keeps styling in the stylesheet, and updates as the DOM changes.

## Verdict

- **Use native CSS** for everything it covers.
- **Use Sass or PostCSS** when you already have a build pipeline and need build-time features.
- **Use CSS Post Processor** when you need selectors and values that depend on the live page, or you want preprocessor features without a build step.
