Media, layout and form selectors

More states the browser knows but CSS can't test. Each replaces a common event handler or measurement script.

Selector Matches Replaces
img:loaded The image has finished loading onload handlers for fade-in effects
img:broken The image failed to load onerror fallbacks
:wrapped A flex or grid item that wrapped below the first row Measuring item positions after every resize
:filled A field has a value (a checkbox or radio is checked) Floating-label scripts, the placeholder=" " trick
:dirty A field differs from its original value "You have unsaved changes" scripts
:offline The browser has lost its connection online/offline listeners
:idle(30s) No mouse, keyboard, touch or scroll input for that long (default 30s) Inactivity timers

img:loaded and img:broken#

CSS
img { transition: opacity .4s; }
img:not(:loaded) { opacity: 0; }          /* fade in once loaded */
img:broken { visibility: hidden; }        /* hide the broken-image icon */
.avatar:has(img:broken) { background: url(/img/default-avatar.svg) center/cover; }

Write the hidden state as :not(:loaded) so images still show in browsers that never run css-pp. Lazy-loaded images (loading="lazy") become :loaded when they actually load.

:wrapped — flex items on a second row#

CSS
/* switch to a hamburger menu the moment the links no longer fit on one line */
nav ul { display: flex; flex-wrap: wrap; }
nav:has(li:wrapped) ul { display: none; }
nav:has(li:wrapped) .menu-button { display: block; }

An item is wrapped when it starts below the bottom of its first sibling. It is re-checked when the container or the window resizes. There is no reliable way to detect this in native CSS.

Avoid loops. If hiding the wrapped items makes them fit again, the state flips back. Hide the list and show a button, as above, rather than shrinking the items themselves.

:filled and :dirty — form fields#

CSS
/* floating labels without placeholder tricks */
.field label { translate: 0 1.6em; transition: translate .2s, font-size .2s; }
.field:has(input:filled, input:focus) label { translate: 0; font-size: .8em; }

/* unsaved-changes banner */
form .unsaved { display: none; }
form:has(:dirty) .unsaved { display: block; }

Both update as the user types, on change, and after a form reset. Qualify them with a form element or a class, as above, so css-pp only checks form fields.

:offline#

CSS
.offline-banner { display: none; }
html:offline .offline-banner { display: block; }

:idle(30s)#

CSS
/* hide video controls and the cursor after 3 seconds without input */
html:idle(3s) .player-controls { opacity: 0; }
html:idle(3s) .player { cursor: none; }

Use seconds (3s), milliseconds (500ms) or minutes (2m). Any mouse movement, click, key press, touch or scroll resets the timer.