Nº 445 of 520
Terrazzo background
A seeded SVG terrazzo floor of irregular stone chips in three depth layers, with a reshuffle button, three stone palettes and gentle pointer parallax.
Preview
Use it
Free to use · credit appreciated1 Once, in your project
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "Terrazzo background" (piece background-terrazzo) in my stack. Match its brief; restyle it onto my product's theme.
No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.
<!-- Design Lounge Nº 445 · "Terrazzo background" · www.designlounge.live -->
# Terrazzo background
> **Build brief for a coding agent.** Rebuild this piece in the reader's stack. If they haven't said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don't "improve" them.
## What it is
A generated terrazzo floor as a full-frame background, shown behind the hero of "Casa Brecce", a small studio that hand-casts terrazzo tiles. One inline `<svg>` holds three `<g>` layers of irregular polygon chips (fine flecks, medium chips, a few large shards) generated from a seeded PRNG, so batch 4182 always looks the same and "Reshuffle chips" pours a new batch. Chips reference five colour classes, not hex values, so switching between the Verona, Nero and Palma stones is a pure CSS variable swap that cross-fades in 420ms. Moving the pointer shifts the three layers by 3, 8 and 16px, so the floor gains a little depth. The copy sits on a cream "sample label" card with a hairline border, which is how a stone sample is actually labelled.
## Reference behaviour
1. Initial state: Verona stone (cream `#EFE6D8` with rust, ochre, sage, charcoal and white chips), batch 4182, label "Sample 012 · Verona".
2. The floor is static until the pointer moves. Pointer position maps to `(-1…1, -1…1)` from the viewport centre; each layer eases toward `offset × depth` (depths 3, 8, 16px) with factor 0.08 per frame, and the loop stops itself once the remaining distance is under 0.002.
3. "Reshuffle chips" picks a new seed in 1000–9999, updates the readout ("Batch 1806"), the label code ("Sample 061 · …", code = seed mod 97 + 1, three digits) and regenerates all chips. The new layers fade in from 0 with stagger 0 / 90 / 180ms over 420ms. Its icon rotates −120° while pressed.
4. Stone radios: Verona, Nero (charcoal `#2A2826` base with white, salmon, brass, slate chips), Palma (plaster pink `#F2C9B8` with bottle green, terracotta, cream, marigold). Background, chip fills, button colour and label name update; colours cross-fade 420ms. Chip geometry does not change.
5. "Pointer parallax" switch (`aria-pressed`, default on) disables the effect; turning it off eases layers back to 0.
6. Resizing regenerates the same seed for the new size after a 150ms debounce.
7. Hidden tab cancels any running parallax frame; it resumes on the next pointer move.
8. `prefers-reduced-motion: reduce`: parallax starts off (switch shows off and can be turned on), reshuffle swaps instantly with no fade, transitions drop to 1ms.
## Structure
```
1280 × 800 svg.floor fixed, inset −24px (bleed for parallax) · .sheen · .page
┌──────────────────────────────────────────────────────────────────────┐
│ ┌ Casa Brecce ──────────────────────── Tiles Worktops The studio Trade ┐ │ nav card 56px, 24px/40px margins
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ┌ label card 600px ───────────────┐ ▲ ◆ ▪ · ▰ · ◣ · ▪ · │
│ │ SAMPLE 012 · VERONA PORTO, PT │ · ▪ ◆ · ▲ · · ◢ ▪ │
│ │─────────────────────────────────│ ▪ · ▲ ▪ · ◆ · · │
│ │ Floors poured │ · ◣ · ▪ · ▰ · ▪ · │
│ │ by hand, chipped │ │
│ │ by chance. 64px serif │ ┌ Mix Batch 4182 ┐ │
│ │ sub 17px, max 460px │ │ [↻ Reshuffle chips]│ │
│ │ [Order a sample box →] │ │ STONE [V][N][P] │ │
│ └─────────────────────────────────┘ │ Pointer parallax ●│ │
└────────────────────────────────────────────────────┴───────────────────┘─┘
page padding 40px; panel 264px, 40px from right and bottom
```
… 181 more lines. Copy or download for the full brief. Loading the source…Brief
221 linesBuild brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.
What it is
A generated terrazzo floor as a full-frame background, shown behind the hero of “Casa Brecce”, a small studio that hand-casts terrazzo tiles. One inline <svg> holds three <g> layers of irregular polygon chips (fine flecks, medium chips, a few large shards) generated from a seeded PRNG, so batch 4182 always looks the same and “Reshuffle chips” pours a new batch. Chips reference five colour classes, not hex values, so switching between the Verona, Nero and Palma stones is a pure CSS variable swap that cross-fades in 420ms. Moving the pointer shifts the three layers by 3, 8 and 16px, so the floor gains a little depth. The copy sits on a cream “sample label” card with a hairline border, which is how a stone sample is actually labelled.
Reference behaviour
- Initial state: Verona stone (cream
#EFE6D8with rust, ochre, sage, charcoal and white chips), batch 4182, label “Sample 012 · Verona”. - The floor is static until the pointer moves. Pointer position maps to
(-1…1, -1…1)from the viewport centre; each layer eases towardoffset × depth(depths 3, 8, 16px) with factor 0.08 per frame, and the loop stops itself once the remaining distance is under 0.002. - “Reshuffle chips” picks a new seed in 1000–9999, updates the readout (“Batch 1806”), the label code (“Sample 061 · …”, code = seed mod 97 + 1, three digits) and regenerates all chips. The new layers fade in from 0 with stagger 0 / 90 / 180ms over 420ms. Its icon rotates −120° while pressed.
- Stone radios: Verona, Nero (charcoal
#2A2826base with white, salmon, brass, slate chips), Palma (plaster pink#F2C9B8with bottle green, terracotta, cream, marigold). Background, chip fills, button colour and label name update; colours cross-fade 420ms. Chip geometry does not change. - “Pointer parallax” switch (
aria-pressed, default on) disables the effect; turning it off eases layers back to 0. - Resizing regenerates the same seed for the new size after a 150ms debounce.
- Hidden tab cancels any running parallax frame; it resumes on the next pointer move.
prefers-reduced-motion: reduce: parallax starts off (switch shows off and can be turned on), reshuffle swaps instantly with no fade, transitions drop to 1ms.
Structure
1280 × 800 svg.floor fixed, inset −24px (bleed for parallax) · .sheen · .page
┌──────────────────────────────────────────────────────────────────────┐
│ ┌ Casa Brecce ──────────────────────── Tiles Worktops The studio Trade ┐ │ nav card 56px, 24px/40px margins
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ┌ label card 600px ───────────────┐ ▲ ◆ ▪ · ▰ · ◣ · ▪ · │
│ │ SAMPLE 012 · VERONA PORTO, PT │ · ▪ ◆ · ▲ · · ◢ ▪ │
│ │─────────────────────────────────│ ▪ · ▲ ▪ · ◆ · · │
│ │ Floors poured │ · ◣ · ▪ · ▰ · ▪ · │
│ │ by hand, chipped │ │
│ │ by chance. 64px serif │ ┌ Mix Batch 4182 ┐ │
│ │ sub 17px, max 460px │ │ [↻ Reshuffle chips]│ │
│ │ [Order a sample box →] │ │ STONE [V][N][P] │ │
│ └─────────────────────────────────┘ │ Pointer parallax ●│ │
└────────────────────────────────────────────────────┴───────────────────┘─┘
page padding 40px; panel 264px, 40px from right and bottom
<svg class="floor" aria-hidden="true">withg#L0(flecks),g#L1(chips),g#L2(shards),viewBox= viewport + 48px.- Inside each
<g>, chips are merged into one<path>per colour class (.k0–.k4): at most 15 path elements total. .sheen: fixed radial highlight (white 22% → 0) suggesting a polished surface..page:<nav aria-label="Main">card;<main class="hero">with.labelcard (p.code,h1,p.sub,a.btn).<form class="ctl" aria-label="Terrazzo controls">: title +<output aria-live="polite">, reshuffle<button>,<fieldset>“Stone” of three radio cards each with a CSS-drawn mini swatch, parallax switch button labelled by its text.
Tokens
:root {
/* Verona (default) */
--base: #efe6d8; /* binder */
--c0: #b4532a; /* rust */
--c1: #d9a441; /* ochre */
--c2: #8a9a7b; /* sage */
--c3: #2e2a26; /* charcoal */
--c4: #fbf6ee; /* white marble */
--card: #fbf6ee; /* nav, label, panel */
--ink: #2a2420;
--ink-2: #5f554c;
--line: #d8ccba; /* hairlines */
--accent: #b4532a; /* primary button */
--accent-ink: #fffaf2;
--serif: "Young Serif", Georgia, serif;
--sans: "Figtree", system-ui, sans-serif;
--fs-display: 64px;
--fs-sub: 17px;
--fs-body: 15px;
--fs-code: 11px;
--pad: 40px;
--r: 2px; /* everything: cut-stone corners */
--bleed: 24px;
--depths: 3px 8px 16px;
--ease: cubic-bezier(.2,.7,.2,1);
--t: 180ms;
--t-mix: 420ms; /* palette cross-fade, reshuffle fade */
}
[data-stone="nero"] { --base: #2a2826; --c0: #f1ece4; --c1: #e59b82; --c2: #b89b5e; --c3: #6f7a80; --c4: #46423e; --accent: #e59b82; --accent-ink: #2a1a14; }
[data-stone="palma"] { --base: #f2c9b8; --c0: #2f6b4f; --c1: #c4532e; --c2: #fbf3e6; --c3: #e8a33d; --c4: #b98b7a; --accent: #2f6b4f; --accent-ink: #f7f2e8; }
Chip layers (counts scale with area):
| Layer | One chip per | Size (radius) px | Size curve | Parallax depth |
|---|---|---|---|---|
| L0 flecks | 800 px² | 1.2 – 4 | min + r^1.8 × range | 3px |
| L1 chips | 2,800 px² | 5 – 15 | same | 8px |
| L2 shards | 13,000 px² | 16 – 42 | same | 16px |
Colour weights (index picked from [0,0,0,1,1,2,2,3,4,4]): c0 30%, c1 20%, c2 20%, c3 10%, c4 20%.
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Headline | Young Serif | 64px | 400 | 1.02 | −0.02em | sentence |
| Wordmark | Young Serif | 20px | 400 | 1 | −0.01em | Title |
| Panel title | Young Serif | 18px | 400 | 1 | 0 | Title |
| Sample code | Figtree | 11px | 600 | 1.5 | +0.14em | UPPERCASE |
| Sub | Figtree | 17px | 400 | 1.55 | 0 | sentence |
| Nav links | Figtree | 14px | 500 | 1.5 | 0 | Title |
| Buttons | Figtree | 15px / 14px | 600 | 1 | 0 | sentence |
| Panel text | Figtree | 13px | 400/500 | 1.5 | 0 | sentence, tabular batch |
Motion
| Element | Trigger | Property | From → To | Duration | Easing | Reduced motion |
|---|---|---|---|---|---|---|
| Layers | pointermove | transform: translate | 0 → ±3 / 8 / 16px | ease 0.08 per frame until settled | exponential | off by default |
| Reshuffle | click | layer opacity | 0 → 1 | 420ms, stagger 0/90/180ms | --ease | instant |
| Stone swap | radio change | body bg, chip fill, button colours | old → new tokens | 420ms | --ease | 1ms |
| Reshuffle icon | :active | rotate | 0 → −120° | 420ms | --ease | 1ms |
| Switch knob | click | translateX | 0 → 18px | 180ms | --ease | 1ms |
| Button | hover | translateY | 0 → −2px | 180ms | --ease | 1ms |
There is no continuous loop. The parallax rAF only runs while layers are still moving.
States
- Default: Verona, batch 4182, parallax on.
- Stone selected: radio card border goes
--inkwith a 1px inset ink ring. - Parallax off: switch track transparent, knob left,
aria-pressed="false"; layers return to 0. - Parallax on: track ink-filled, knob card-coloured and right,
aria-pressed="true". - Reshuffle hover: fills with ink, text goes card; active: icon spins.
- Focus-visible:
outline: 2px solid var(--ink); outline-offset: 3pxon links, buttons, and the radio card span. - Hidden tab: parallax frame cancelled.
Accessibility
- The floor and sheen are
aria-hidden="true"; the floor has no pointer events. - The batch readout is
aria-live="polite", so a reshuffle announces “Batch 1806”. - Stone is a radio group in a
<fieldset>with visible legend “Stone”; each card’s visible name (“Verona”) is the label. - The parallax switch is a
<button aria-pressed>labelled by the “Pointer parallax” text. - All copy sits on the opaque card, never on chips:
#2A2420on#FBF6EEis 14.2:1,#5F554Cis 6.8:1. Button text:#FFFAF2on#B4532A4.8:1;#2A1A14on#E59B827.4:1;#F7F2E8on#2F6B4F5.6:1. - Tab order: wordmark → 4 nav links → Order a sample box → Reshuffle → stone radios → parallax switch.
- Hit targets: reshuffle 44px tall, radio cards ~60px, switch row 40px.
Responsive rules
- ≥ 1280: as specified.
- 1024–1279: label card 540px, headline 56px.
- 768–1023: label card 480px; panel stays bottom-right.
- < 760: nav and card go full width with 12px margins, nav links hide, card padding 24px, headline 40px, sub 15px; panel spans the bottom; the parallax switch hides (touch has no hover pointer). Hero bottom padding 280px keeps the card clear of the panel.
- Chip counts scale with area, so density is the same at every size; regenerate on resize with the same seed.
Acceptance checklist
Always
- Chips come from a seeded PRNG; the same seed and size give the same floor.
- Chips are irregular 4–7 vertex polygons with jittered radius and squash, never circles.
- Three layers with different chip sizes and parallax depths.
- Chips are grouped into one
<path>per colour class; palette swap touches only CSS variables. - The SVG bleeds 24px past each edge so parallax never shows a gap.
- No continuous animation loop; parallax rAF stops when settled and when hidden.
- Reduced motion: parallax off by default, reshuffle instant.
- Copy sits on an opaque card with ≥ 4.5:1 contrast.
- Batch readout is a polite live region.
This demo
- Opens on Verona
#EFE6D8with batch 4182 and “Sample 012 · Verona”. - Stones Verona, Nero (
#2A2826), Palma (#F2C9B8) with the chip colours in Tokens. - Parallax depths 3 / 8 / 16px; densities 800 / 2,800 / 13,000 px² per chip.
- Headline “Floors poured by hand, chipped by chance.” at 64px Young Serif.
- Reshuffle fades layers in with 90ms stagger over 420ms.
Implementation notes
Seeded PRNG and chip shape. mulberry32 is enough; draw vertices around a circle with angular and radial jitter, then squash Y a little:
const rng = a => () => { a |= 0; a = a + 0x6D2B79F5 | 0;
let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296; };
function chip(r, x, y, s) {
const n = 4 + Math.floor(r() * 4), rot = r() * 6.283, pts = [];
for (let i = 0; i < n; i++) {
const a = rot + i / n * 6.283 + (r() - .5) * .9, d = s * (.55 + r() * .45);
pts.push(`${x + Math.cos(a) * d} ${y + Math.sin(a) * d * (.7 + r() * .3)}`);
}
return 'M' + pts.join('L') + 'Z';
}
One path per colour. Concatenate sub-paths so a layer is at most five DOM nodes:
const byColour = [[], [], [], [], []];
for (let i = 0; i < n; i++) {
const s = cfg.min + Math.pow(r(), 1.8) * (cfg.max - cfg.min);
byColour[WEIGHT[Math.floor(r() * WEIGHT.length)]].push(chip(r, r() * W, r() * H, s));
}
g.innerHTML = byColour.map((d, k) => d.length ? `<path class="k${k}" d="${d.join('')}"/>` : '').join('');
Self-stopping parallax.
function frame() {
pos.x += (pos.tx - pos.x) * .08; pos.y += (pos.ty - pos.y) * .08; apply();
raf = Math.abs(pos.tx - pos.x) + Math.abs(pos.ty - pos.y) > .002 && !document.hidden
? requestAnimationFrame(frame) : 0;
}
Common mistakes:
- 2,000 separate
<path>or<circle>elements; palette swaps and parallax get slow. - Circles or rounded blobs: that’s confetti, not stone.
- Uniform chip size. Terrazzo needs many flecks, some chips and a few big shards.
Math.random()in the generator: resize or palette swap then reshuffles the floor by accident.- Baking hex colours into the paths, so changing stone means regenerating.
- Putting copy straight on the chips. Use the label card.
- No bleed: the 16px layer slides and shows the bare edge.
Details
- Palette
- Type
- Young Serif · Figtree
- Motion
- Subtle motion
- Build
- One session
- Tags
Studied
Susan AcharyaDraw one scene and let it move a little. The words stay calm on top.
All sourcesOn the shelves