Nº 219 of 520
Diagonal boxes background
A monochrome isometric box field on canvas. Boxes near the pointer rise on taller sides and lighten, then settle back when it leaves.
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 "Diagonal boxes background" (piece background-diagonal-boxes) 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º 219 · "Diagonal boxes background" · designlounge.vercel.app -->
# Diagonal boxes 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 full-frame isometric cube pattern drawn on one `<canvas>` behind a short hero for a fictional architecture practice, "Cubit Studio". Every tile is a three-face cube in one hue: a light top, a mid left face, a dark right face. Cubes within 230px of the pointer **rise**: the top face moves up by up to 24px while both side faces stretch to meet it, and the faces lighten in step. The field looks like a relief model pressed up from below. The detail worth copying is that nothing is faked with CSS 3D: each cube is three polygons, drawn back-to-front row by row, so a raised cube correctly overlaps the cube behind it and is overlapped by the cube in front. A floating panel switches Graphite and Chalk tones, the box size (S/M/L), and the lift height.
## Reference behaviour
1. First frame: graphite tone, medium boxes (side 30px), and a cluster already raised around (74%, 62%) of the viewport, so the effect is visible before the pointer arrives.
2. Moving the pointer re-targets every cube's lift from its distance to the pointer, with a smoothstep falloff over 230px. Each cube eases 16% of the way to its target per frame.
3. The animation loop runs only while any cube is still more than 0.05px from its target. When everything settles, `requestAnimationFrame` stops. An idle page uses no CPU.
4. Leaving the window drops every target to 0, and the field settles flat.
5. Tone: Graphite / Chalk swaps the three face ramps and the page tokens instantly. Exactly one is pressed.
6. Box size: S 20px, M 30px, L 44px side. The grid rebuilds and the current lift is applied without easing, so the change reads as a swap.
7. Lift slider: 0–48px in steps of 4, default 24. The value is shown as "24 px" next to the label. 0 makes the field a flat pattern.
8. Reduced motion: the pointer does not move the field. The initial raised cluster stays as a static composition, and the hint reads "Reduced motion: the field stays still." Tone, size and lift still redraw one static frame.
9. Links and buttons over the field have 160ms colour transitions and a 2px ink focus ring.
## Structure
```
1280 × 800 (canvas fixed, inset 0)
┌──────────────────────────────────────────────────────────────────────┐
│ ⬡ Cubit Studio WORK STUDIO JOURNAL CONTACT │ nav 84
│ ┌ panel 236 ────┐ │
│ [ARCHITECTURE & INTERIORS · PORTO] scrim chip │ TONE [G][C] │ │ top 112
│ │ BOX SIZE [S M L]│ │ right 64
│ We build rooms 88px │ LIFT ───◆── 24 │ │
│ that hold still. │ hint │ │
│ [sub 18px on scrim, max 470] └────────────────┘ │
│ [See the 2026 projects →] [Book a site visit] ▲▲▲ raised cluster │
│ ▲▲▲▲▲ │
├───────────────────────┬───────────────────────┬──────────────────────┤
│ Casa Lumen, Gaia 2025 │ Reading room, Braga │ Atelier Rua Nove │ project strip
└───────────────────────┴───────────────────────┴──────────────────────┘
side padding 64px; strip bottom margin 40px
… 177 more lines. Copy or download for the full brief. Loading the source…Brief
217 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 full-frame isometric cube pattern drawn on one <canvas> behind a short hero for a fictional architecture practice, “Cubit Studio”. Every tile is a three-face cube in one hue: a light top, a mid left face, a dark right face. Cubes within 230px of the pointer rise: the top face moves up by up to 24px while both side faces stretch to meet it, and the faces lighten in step. The field looks like a relief model pressed up from below. The detail worth copying is that nothing is faked with CSS 3D: each cube is three polygons, drawn back-to-front row by row, so a raised cube correctly overlaps the cube behind it and is overlapped by the cube in front. A floating panel switches Graphite and Chalk tones, the box size (S/M/L), and the lift height.
Reference behaviour
- First frame: graphite tone, medium boxes (side 30px), and a cluster already raised around (74%, 62%) of the viewport, so the effect is visible before the pointer arrives.
- Moving the pointer re-targets every cube’s lift from its distance to the pointer, with a smoothstep falloff over 230px. Each cube eases 16% of the way to its target per frame.
- The animation loop runs only while any cube is still more than 0.05px from its target. When everything settles,
requestAnimationFramestops. An idle page uses no CPU. - Leaving the window drops every target to 0, and the field settles flat.
- Tone: Graphite / Chalk swaps the three face ramps and the page tokens instantly. Exactly one is pressed.
- Box size: S 20px, M 30px, L 44px side. The grid rebuilds and the current lift is applied without easing, so the change reads as a swap.
- Lift slider: 0–48px in steps of 4, default 24. The value is shown as “24 px” next to the label. 0 makes the field a flat pattern.
- Reduced motion: the pointer does not move the field. The initial raised cluster stays as a static composition, and the hint reads “Reduced motion: the field stays still.” Tone, size and lift still redraw one static frame.
- Links and buttons over the field have 160ms colour transitions and a 2px ink focus ring.
Structure
1280 × 800 (canvas fixed, inset 0)
┌──────────────────────────────────────────────────────────────────────┐
│ ⬡ Cubit Studio WORK STUDIO JOURNAL CONTACT │ nav 84
│ ┌ panel 236 ────┐ │
│ [ARCHITECTURE & INTERIORS · PORTO] scrim chip │ TONE [G][C] │ │ top 112
│ │ BOX SIZE [S M L]│ │ right 64
│ We build rooms 88px │ LIFT ───◆── 24 │ │
│ that hold still. │ hint │ │
│ [sub 18px on scrim, max 470] └────────────────┘ │
│ [See the 2026 projects →] [Book a site visit] ▲▲▲ raised cluster │
│ ▲▲▲▲▲ │
├───────────────────────┬───────────────────────┬──────────────────────┤
│ Casa Lumen, Gaia 2025 │ Reading room, Braga │ Atelier Rua Nove │ project strip
└───────────────────────┴───────────────────────┴──────────────────────┘
side padding 64px; strip bottom margin 40px
<canvas aria-hidden="true">fixed, full viewport, backing store ×min(devicePixelRatio, 2)..pageis a flex column withpointer-events: none; links and buttons re-enable them so pointer moves always reachwindow.<nav aria-label="Main">with brand (inline cube glyph) and four links.<main class="hero">: kicker chip,<h1>, sub on a scrim, two links.<ul class="work" aria-label="Recent projects">: three items with name and mono meta..panel role="group": two segmented groups ofaria-pressedbuttons and a labelled range input with an<output>.
Tokens
:root {
/* graphite — the three faces, flat */
--face-top: #2b2d31;
--face-left: #222428;
--face-right: #1a1b1e; /* also page background */
/* raised ramp ends (top / left / right at full lift) */
--face-top-hi: #686b72;
--face-left-hi: #3d3f45;
--face-right-hi: #2b2d31;
--ink: #ecedef; /* headline, active segment fill */
--ink-2: #a3a6ac; /* sub, links, labels */
--ink-3: #7b7e85; /* project meta, hint */
--line: rgba(236, 237, 239, .14);
--scrim: rgba(26, 27, 30, .78); /* behind chip, sub, panel, buttons */
--font: "Familjen Grotesk", system-ui, sans-serif;
--mono: "Spline Sans Mono", ui-monospace, monospace;
--box-s: 20px; --box-m: 30px; --box-l: 44px;
--lift-default: 24px; --lift-max: 48px;
--lift-radius: 230px;
--ease-k: .16; /* per-frame approach factor */
--ease: cubic-bezier(.2, .7, .2, 1);
--t-fast: 160ms;
--r: 2px;
}
:root[data-tone="chalk"] {
--face-top: #ecebe6; --face-left: #d6d3cc; --face-right: #bfbbb2;
--face-top-hi: #ffffff; --face-left-hi: #e6e4de; --face-right-hi: #d0cdc5;
--ink: #18191b; --ink-2: #45474c; --ink-3: #5d6066;
--line: rgba(24, 25, 27, .16); --scrim: rgba(236, 235, 230, .82);
}
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Headline | Familjen Grotesk | 88px | 500 | 0.95 | −0.045em | sentence, balanced |
| Sub | Familjen Grotesk | 18px | 400 | 1.55 | 0 | sentence |
| Brand | Familjen Grotesk | 19px | 600 | 1 | −0.02em | Title |
| Buttons | Familjen Grotesk | 15px | 500 | 1 | 0 | sentence |
| Project name | Familjen Grotesk | 15px | 500 | 1.5 | 0 | Title |
| Nav links, kicker | Spline Sans Mono | 12px | 500 | 1 | 0.08em | UPPER |
| Panel labels | Spline Sans Mono | 11px | 500 | 1 | 0.06em | UPPER |
| Project meta | Spline Sans Mono | 12px | 400 | 1 | 0 | as written |
Motion
| Thing | Trigger | Property | From → To | Duration | Easing | Reduced motion |
|---|---|---|---|---|---|---|
| Cube lift | pointermove | top face y, side face height, face colour step | current → target (0–lift) | until within 0.05px | exponential approach, 16% per frame | off; static cluster |
| Field settle | pointerleave | lift | current → 0 | until settled | same | off |
| Size change | click S/M/L | grid rebuild | — | instant | — | instant |
| Tone change | click | ramps, tokens | — | instant | — | instant |
| Buttons, links | hover | colour, background | — | 160ms | --ease | 1ms |
Falloff per cube, d = distance from pointer to the cube’s top-face centre:
u = 1 − d / 230 (only when d < 230)
target = lift × u² × (3 − 2u)
colour step k = round(current / 48 × 16 × 1.6), clamped to 16
States
- Flat: all cubes at their base colours.
- Raised: top face shifted up by
l, side faces taller byl, all three faces using ramp stepk(17 steps from base to hi). - Segment pressed: filled
--ink, text--face-right. Resting segments are transparent with--ink-2text; hover brings text to--ink. - Range: 1px track
--ink-3; 14px square thumb rotated 45° in--ink, so the thumb is itself a tiny box. - Focus-visible: 2px
--inkoutline, offset 3px; inside segments, offset −2px. - Buttons: outline button fills with
--inkon hover; primary fades to--ink-2.
Accessibility
- The canvas is decorative (
aria-hidden="true"). No information lives only in the field. - The motion is pointer-driven and stops when the pointer stops, so no pause control is required; reduced motion disables it entirely.
- Panel:
role="group"labelled “Background controls”; each segmented set is arole="group"with its own label; buttons usearia-pressed. The lift slider has a real<label for>and an<output for>that reads “24 px”. - Tab order: brand → four nav links → two hero links → Graphite → Chalk → S → M → L → Lift.
- Text sits on
--scrimbackings (chip, sub-copy, buttons, project strip), so contrast does not depend on what the cubes are doing:#a3a6acon the scrim over graphite is 7:1,#45474con the chalk scrim is 8:1. - Segment buttons are 32px tall; the slider has a 24px hit band.
Responsive rules
- ≥ 1280: as specified. The grid is rebuilt on resize from the box size, so cubes never scale; the field only gains or loses tiles.
- 1024–1279: headline 68px, the third project hides.
- 768–1023: the project strip hides; the panel docks to bottom-right (24px insets).
- < 640: 24px page padding, nav links hide, headline 46px, sub 16px. The panel spans the bottom (24px insets) as a two-column grid: Tone and Box size side by side, Lift full width, the hint hidden.
- Cap the backing store at DPR 2. Cull cubes whose centre is more than two half-widths outside the viewport.
Acceptance checklist
Always
- Cubes are three polygons each (top rhombus, left face, right face) on a pointy-top hex lattice: column step
side × √3, row stepside × 1.5, odd rows offset by half a column. - Rows are drawn top to bottom so raised cubes overlap correctly.
- A raised cube moves only its top face; the bottom vertex stays fixed and the sides stretch.
- Face colours come from precomputed ramps. No colour strings are built per cube per frame.
- The rAF loop stops when all cubes are within 0.05px of target.
- Pointer leaving the window flattens the field.
- Reduced motion: no pointer response, a static composition, controls still redraw.
- All copy sits on a scrim, readable in both tones.
- No horizontal scroll at 375px.
This demo
- First frame: graphite, side 30px, a raised cluster centred at (74%, 62%).
- Headline “We build rooms that hold still.” at 88px.
- Lift default 24px, range 0–48 step 4; sizes 20 / 30 / 44px.
- Chalk tone top face
#ecebe6, raised to#ffffff. - Projects: Casa Lumen, Gaia · Reading room, Braga · Atelier Rua Nove.
Implementation notes
Lattice and cube geometry. Store cells flat as [cx, cy, current, target]:
const h = S * Math.sqrt(3) / 2; // half the hex width
const rows = Math.ceil((H + S * 2 + 60) / (S * 1.5)) + 1;
const cols = Math.ceil(W / (h * 2)) + 2;
for (let r = 0; r < rows; r++)
for (let c = 0; c < cols; c++)
cells.push(c * h * 2 - (r % 2 ? 0 : h), r * S * 1.5 - S, 0, 0);
// per cube, l = current lift
top: (x, y-S-l) (x+h, y-S/2-l) (x, y-l) (x-h, y-S/2-l)
left: (x-h, y-S/2-l) (x, y-l) (x, y+S) (x-h, y+S/2)
right: (x, y-l) (x+h, y-S/2-l) (x+h, y+S/2) (x, y+S)
Self-stopping loop. Only run frames while something is moving:
function frame() {
let moving = false;
for (let i = 0; i < cells.length; i += 4) {
const d = cells[i + 3] - cells[i + 2];
if (Math.abs(d) > .05) { cells[i + 2] += d * .16; moving = true; }
else cells[i + 2] = cells[i + 3];
}
draw();
raf = moving ? requestAnimationFrame(frame) : 0;
}
function update() { targets(); if (!raf) raf = requestAnimationFrame(frame); }
Colour ramps once per tone. Interpolate 17 rgb() strings per face when the tone changes and index them with the lift step. Building strings inside the draw loop is the usual reason this effect drops frames.
Common mistakes:
- Drawing columns instead of rows. Raised cubes then poke through the cubes in front of them.
- Translating the whole cube up. It floats and leaves a hole; the base must stay put.
- Adding a drop shadow or a colour accent. The piece is monochrome on purpose; the lift and the lighter faces are the only signal.
- A loop that runs forever at 60fps on an idle page.
- Using CSS
transform: rotateX()tiles, which breaks the overlap and costs a layer per tile. - Leaving the first frame flat, so the viewer sees a wallpaper and does not know it reacts.
Details
- Palette
- Type
- Familjen Grotesk · Spline Sans Mono
- Motion
- Subtle motion
- Build
- One session
- Tags
Studied
Lama LamaA tiny nav bar can carry the voice. Change its one line for each section.
All sourcesOn the shelves