# Blur-up image loading (`blur-up`)

A tiny blurred version of each photo (a 20 px JPEG, ~0.7 KB inlined) sits in a box of the right shape until the real photo has decoded, then the photo fades in. No grey boxes, no pop, no layout shift.

| | |
|---|---|
| Tag | Flow |
| Size | 1.1 KB gzipped own code (JS 0.7 KB + CSS 0.4 KB; 2.1 KB raw) |
| Dependency | none (vanilla + native APIs) |
| Best for | Every photo below the hero: galleries, work photos, team, review photos |
| Demo | `index.html` in this folder (live: https://wbl-code-lib.pages.dev/modules/blur-up/) |

## What it does
The figure carries the aspect ratio and the inline LQIP as its background (blurred by a pseudo-element, so the blur never touches the photo). The img starts at opacity 0 only when JS is on; JS waits for load + img.decode() before fading it in, so the fade never shows a half-painted image. Cached photos show instantly; a failed photo shows its alt text instead of a blur forever.

## When to use
- Any lazy-loaded photo. Make the LQIPs at build time (one line, below).
- Pair with Gallery lightbox and Photo review cards.

## Don't
- On the LCP hero: give it fetchpriority="high" and no fade (a fading LCP delays it).
- Placeholders bigger than ~1 KB each (20 px wide is plenty).

## Reduced motion, no JS, accessibility
- Reduced motion: the photo appears without the fade.
- No JS: photos load normally over the placeholder (they're never hidden).
- Alt text stays on the real img; the placeholder is a background (ignored by screen readers).

## Notes
- Make an LQIP with Pillow: im.thumbnail((20,20)); im.save(buf,'JPEG',quality=40) then base64 → data:image/jpeg;base64,… (see tools/resize.py). The builder's photo pipeline can emit it next to each photo.

## Relation to the effects libraries
New. (The batch 1 photos have -lqip.jpg files but no loader.)

## Use it
Copy this folder to the site as `/wbl/blur-up/` (keep `vendor/` if there is one), then:

```html
<!-- Blur-up image loading (WBL Code Library) -->
<!-- in <head>: -->
<script>document.documentElement.classList.add('wbl-js')</script>
<link rel="stylesheet" href="/wbl/blur-up/module.css">
<script src="/wbl/blur-up/module.js" defer></script>

<!-- markup: -->
<figure class="wbl-bu" style="aspect-ratio:3/2;background-image:url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDABQODxIPDRQSEBIXFRQYHjIhHhwcHj0sLiQySUBMS0dARkVQWnNiUFVtVkVGZIhlbXd7gYKBTmCNl4x9lnN+gXz/2wBDARUXFx4aHjshITt8U0ZTfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHz/wAARCAANABQDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwCvbTvHeBAXZGGCuc4NS3WoCCFnXa5BwFzyaPESCyv4JbYBCWDEDoTmql6ojmLxjarHIX0rKCktGa1OV6oYbq4lJZkYegHHFFOU5GTRW6MD/9k=)">
  <img src="../_demo/img/comb-red.jpg" srcset="../_demo/img/comb-red-640.jpg 640w, ../_demo/img/comb-red.jpg 1400w" sizes="(min-width: 900px) 50vw, 100vw"
       width="1400" height="933" alt="Comb through freshly coloured copper hair" loading="lazy" decoding="async">
</figure>
```

Colours come from CSS variables with safe defaults: `--wbl-accent`, `--wbl-on-accent`, `--wbl-ink`, `--wbl-paper`,
`--wbl-card`, `--wbl-line`, `--wbl-muted`, `--wbl-band`, `--wbl-radius`, `--wbl-head` (see the library README for the
one-line bridge to the preview builder's `--a / --ink / --bg …`).

## License
Our code: WBL-owned, original. No third-party code.
