# CSS recipes — copy-paste solutions for common UI problems

> Copy-paste CSS recipes: active nav link, shrink header on scroll, reveal on scroll, lazy backgrounds, floating labels, equal-height cards, fit text and more.

Source: https://csspp.dev/recipes/


Short answers to problems that usually need a script. Each one is a few lines of CSS once [CSS Post Processor](/docs/) is on the page.

## Navigation and headers

### Highlight the active navigation link

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

Also matches parent sections: "Docs" stays active on every `/docs/...` page. [More](/docs/scroll-navigation/#current)

### 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); }
```

### Switch to a hamburger menu when links stop fitting

```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; }
```

### Disable carousel arrows at the ends

```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; }
```

### Current year in the footer

```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; }
```

```js
css.on('click', '.chip::after', function () { this.remove(); });
```

### Update a badge drawn with ::after

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

### Close a modal when the backdrop is clicked

```js
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; }
```
