CSS recipes

Short answers to problems that usually need a script. Each one is a few lines of CSS once CSS Post Processor is on the page.

CSS
nav a:current { font-weight: 700; border-bottom: 2px solid currentColor; }

Also matches parent sections: "Docs" stays active on every /docs/... page. More

Shrink the header after scrolling#

CSS
header { height: 80px; transition: height .2s; }
html:scrolled(80px) header { height: 56px; }

Hide the header on scroll down, show it on scroll up#

CSS
header { position: sticky; top: 0; transition: translate .25s; }
html:scrolling-down header { translate: 0 -100%; }

Shadow only while a sticky header is stuck#

CSS
header { position: sticky; top: 0; }
header:stuck { box-shadow: 0 2px 12px rgb(0 0 0 / .15); }

Solid header once the hero has scrolled away#

CSS
body:has(.hero:not(:in-view)) header { background: #fff; }

Highlight the current section in a table of contents#

CSS
.toc a:target-in-view { color: var(--accent); }
CSS
nav ul { display: flex; flex-wrap: wrap; }
nav:has(li:wrapped) ul { display: none; }
nav:has(li:wrapped) .menu-button { display: block; }

Scrolling and reveal effects#

Fade elements in the first time they scroll into view#

CSS
.reveal { transition: opacity .6s, translate .6s; }
.reveal:not(:seen) { opacity: 0; translate: 0 2rem; }

Lazy-load a background image just before it's visible#

CSS
.hero:seen(300px) { background-image: url(/img/hero-large.jpg); }

The image starts downloading when the element is within 300px of the viewport.

Scroll shadows on a scrollable list#

CSS
.list { overflow: auto; max-height: 20rem; }
.list:not(:scroll-end) { box-shadow: inset 0 -16px 12px -12px #0003; }
CSS
.carousel:scroll-start .prev, .carousel:scroll-end .next { opacity: .3; pointer-events: none; }

Stagger an entrance animation#

CSS
.list > li { animation: rise .5s both; animation-delay: calc($[index] * 80ms); }

Images#

Fade images in when they finish loading#

CSS
img { transition: opacity .4s; }
img:not(:loaded) { opacity: 0; }

Hide broken images, or show a fallback#

CSS
img:broken { visibility: hidden; }
.avatar:has(img:broken) { background: url(/img/default-avatar.svg) center / cover; }

A different background for every slide#

CSS
.slider > div { background-image: url(/img/slide-$[index].jpg); }

Background image from a data attribute (CMS-friendly)#

HTML
<div class="card" data-bg="/uploads/photo-17.jpg"></div>
CSS
.card { background: url($[attr(data-bg)]) center / cover; }

A random hero image on each visit#

CSS
.hero { background-image: url(/img/hero-$[random(1,5)].jpg); }

Layout and text#

Equal-height cards across rows#

CSS
.card h3 { height: same; }

Every matched element gets the height of the tallest one, updated on resize.

Make a headline fill its box#

CSS
.hero h1 { font-size: fit(24px, 120px); }

Split a row evenly however many columns there are#

CSS
.cols > div { width: calc(100% / $[count]); }

Show "Read more" only when text is truncated#

CSS
.excerpt { -webkit-line-clamp: 3; display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; }
.post .more { display: none; }
.post:has(.excerpt:overflowing) .more { display: inline; }
CSS
.copyright::after { content: "© $[year] Your Company"; }

Pseudo-elements#

Make an ::after "×" close a chip#

CSS
.chip::after { content: "×"; margin-left: .5em; cursor: pointer; }
.chip::after:hover { color: #dc2626; }
JavaScript
css.on('click', '.chip::after', function () { this.remove(); });

Update a badge drawn with ::after#

JavaScript
css.pseudo(document.querySelector('.inbox'), '::after').text = unread + ' new';

Close a modal when the backdrop is clicked#

JavaScript
css.on('click', 'dialog::backdrop', function () { this.close(); });

Highlight an icon drawn with ::before on hover#

CSS
.link::before { content: "→ "; transition: color .15s; }
.link::before:hover { color: var(--accent); }

Forms and content#

Floating labels#

CSS
.field:has(input:filled, input:focus) label { translate: 0 -1.2em; font-size: .8em; }

Warn about unsaved changes#

CSS
form .unsaved { display: none; }
form:has(:dirty) .unsaved { display: block; }

Dim sold-out products#

CSS
.product:has(.badge:contains("sold out" i)) { opacity: .5; }

Low-stock badge#

CSS
.product .low-stock { display: none; }
.product:has([data-stock<5]) .low-stock { display: inline-block; }

Offline banner#

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

Hide video controls after a few idle seconds#

CSS
html:idle(3s) .player-controls { opacity: 0; }