v3.1.0 New: :current, :scrolled, img:loaded, fit(), $[random()]

CSS that knows your page.

CSS Post Processor is a 8.8 KB script that extends CSS in the browser. It adds variables with JavaScript, nesting, per-element values like $[index], and the selectors native CSS still can't express: whether an element has been seen, is stuck, is cut off, or contains some text.

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/css-pp.min.js"></script>
  • No build step
  • Zero dependencies
  • MIT licensed
  • In production since 2016
style.css
/* one image per slide, any number of slides */
.slider > div {
  background-image: url(/img/slide-$[index].jpg);
}

/* fade in the first time it scrolls into view */
.card:not(:seen) { opacity: 0; translate: 0 2rem; }

/* shadow only while the sticky header is stuck */
header:stuck { box-shadow: 0 2px 12px #0003; }

/* "Read more" only when the text is cut off */
.post:has(.excerpt:overflowing) .more {
  display: inline;
}

/* style by text and by numbers */
.item:has(.badge:contains("sold out" i)) {
  opacity: .5;
}
.item:has([data-stock<5]) .low { display: block; }

What it adds

One line of CSS instead of a script

Every feature below replaces JavaScript you would otherwise write and maintain: an IntersectionObserver here, a scroll listener there, a loop that sets inline styles. With CSS Post Processor you write a selector and the browser does the rest.

:seen and :in-view

Animate elements when they scroll into view, once or every time, with an optional visibility threshold. No animation library needed.

State selectors →

:stuck

Style a position: sticky header or sidebar only while it is pinned. Works at the top or bottom, in the page or inside a scrolling box.

Sticky detection →

:overflowing

Know when text is truncated or a box overflows. Show "read more" or a tooltip only when the content is actually cut off.

Overflow detection →

:contains("text")

Select elements by their text, with an optional case-insensitive flag. It stays live as the text changes and works inside :has().

Content selectors →

[data-price>=100]

Compare attribute values as numbers with <, <=, > and >=. Highlight prices, low stock, ratings and scores without a line of JavaScript.

Numeric attributes →

$[index]

A per-element counter you can use anywhere, even inside url(). Image sequences, staggered animations and color ramps take one rule.

Per-element values →

Variables with JavaScript

A variable can be a value, a mixin with parameters, or a JavaScript expression, such as the viewport width or an element's height. Change it at runtime with css.set().

Variables & mixins →

Responsive variables

Add ! to a variable name and it is recalculated on resize. Pick the right image size for the screen in a single expression.

Auto-updating values →

Nesting with &

Nest rules with &:hover, .dark & or &-title, and put @media inside a rule. The output is flat CSS that works in every browser.

Nesting →

:current and :scrolled

Highlight the active nav link, shrink the header after scrolling or hide it on the way down. Scroll shadows and scrollspy menus too.

Scroll & navigation →

img:loaded and :wrapped

Fade images in once they load, hide broken ones, switch to a hamburger the moment the nav wraps, float labels on filled fields.

Media, layout & forms →

fit() and height: same

Make a headline fill its box and give cards equal heights across rows. Both recalculate on resize and when fonts load.

Layout helpers →

Works with :has()

Target parents based on things CSS can't see

Native :has() lets you style a parent based on its children. CSS Post Processor's selectors compile to plain attribute selectors, so they work inside :has(). You can style a header when the hero leaves the screen, or a card when its text is cut off.

  • The parent of a moving element: :has(> .child), native CSS
  • The parent of an element that has been seen: :has(> .x:seen)
  • Earlier siblings of an element: :has(~ .child), native CSS
  • A card whose price is over 100: .card:has([data-price>100])
How to select a parent in CSS
header.css
/* Solid header once the hero has scrolled away */
body:has(.hero:not(:in-view)) header {
  background: #fff;
  box-shadow: 0 1px 8px rgb(0 0 0 / .08);
}

/* Highlight the list item that holds the active link */
nav li:has(> a.active) { border-left: 3px solid; }

/* Every item before the selected one */
.steps li:has(~ li.current) { color: green; }

How it works

Plain CSS out, a few observers in

CSS Post Processor reads your stylesheet in the browser and writes plain CSS into a <style> element. Its selectors become attribute selectors such as [data-css-seen], which small observers switch on and off. The browser's own engine still does the matching.

  1. Add one script

    Self-host the 8.8 KB file or load it from a CDN. Nothing to install, and no bundler or config.

  2. Open your stylesheet

    css.open('/css/site.css') fetches it, or css.open('#id') processes an inline <style>.

  3. Write the CSS you wanted

    A 74 KB stylesheet with variables and nesting compiles in about 9 ms on a desktop. Rules update live as the DOM changes.

8.8 KB

gzipped (24.5 KB minified)

0

dependencies, no build step

~9 ms

to process a 74 KB stylesheet

Honest fit

When to use it, and when not to

Great fit

  • Sites without a build pipeline: plain HTML, WordPress, PHP, Perl or any CMS theme
  • Scroll reveals, sticky-header states, truncation hints and content-based styling without custom JavaScript
  • Image or color sequences and staggered animations driven by element position
  • Values that depend on the viewport or other elements at runtime

Look elsewhere if

  • Your Content Security Policy forbids 'unsafe-eval' and you need JavaScript expressions in variables (nesting and the new selectors still work)
  • Every style must exist before JavaScript runs, for example in critical CSS for an app shell
  • You already compile with Sass and only need build-time variables or loops
  • Native CSS already covers what you need, which it often does in 2026: variables, nesting, :has(), container queries

Quick start

Working in under a minute

Add the script, point it at your stylesheet and use the new syntax. Any CSS you already have keeps working unchanged.

index.html
<script src="/js/css-pp.min.js"></script>
<script>
  css.defaults.debug = 0;      // quiet errors in production
  css.open('/css/site.css');   // fetch, process, inject
</script>

Ship the CSS you actually wanted to write

Free and open source under the MIT License. Self-host it, load it from a CDN or install it from npm.