Nº 640 of 677
Tessel sliding tile puzzle
A playable 4×4 sliding puzzle of an SVG harbour scene: spring-snapped tiles, tap, swipe or arrow keys, peek, pause, a solved ripple and a result card.
Preview
Use it
Free to use · credit appreciated1 Once, in your project · Node 22.20+
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "Tessel sliding tile puzzle" (piece minigame-tile-slide) 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º 640 · "Tessel sliding tile puzzle" · www.designlounge.live -->
# Tessel sliding tile puzzle
> **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 complete phone mini-game: Tessel, a sliding number puzzle where fifteen tiles each carry a slice of an original flat SVG picture (Harbour Light: a striped lighthouse on a cliff, a low sun, a sailboat on a dusk sea) and their number. Stone paper ground, ink frame, one green for "in place", one monospace family for everything. The layout follows iOS 26 conventions without glass: a quiet header, a hairline HUD, the board pushed down into the thumb zone, two 56px buttons under it. The details worth copying are the spring snap (tiles overshoot by a hair and settle in 260ms), number chips that turn green when a tile is home, and a 15-segment progress bar that fills as you go, so a hard puzzle always shows you how close you are.
## Reference behaviour
1. Initial state: a full shuffle (220 random legal moves from solved, so it is always solvable), MOVES 0, TIME 0:00, BEST 1:58. One tile next to the gap has a 3px cream ring and nudges 8px towards the gap every 1.4s. The line under the board reads "**Slide** a tile into the gap".
2. Tap a tile in the same row or column as the gap: it and every tile between it and the gap slide one cell towards the gap together. Each tile moved adds 1 to MOVES. Taps on tiles not in line with the gap do nothing.
3. Swipe on the board (24px or more; the larger axis wins): the tile on the far side of the gap moves into it in the swipe direction. A swipe never also counts as a tap.
4. Keyboard: ArrowLeft moves the tile right of the gap left, ArrowRight the tile left of the gap right, ArrowUp the tile below up, ArrowDown the tile above down. Space pauses, P toggles peek, Escape resumes.
5. The first move starts the timer and removes the hint; the line becomes "Tap, swipe or use the **arrow keys**".
6. After every move: each tile in its home cell shows a green number chip; the progress card reads "N of 15 tiles in place" and N of its 15 segments turn green.
7. **Peek** (toggle, `aria-pressed`): the solved picture covers the board, labelled "Solved picture", fading in over 200ms. Tap again to hide. The timer keeps running.
8. **Pause** (top-right, 44px): an ink panel covers the board so the picture cannot be studied, with Resume, Restart this shuffle and New easy shuffle (24 moves). The timer stops; Resume or Escape continues it.
9. **Shuffle** deals a new full shuffle and resets moves and time.
10. Solving: the hidden 16th tile fades in to complete the picture, every tile pulses to 1.07 in a diagonal ripple (55ms per step from top-left), number chips fade away after 600ms, and the line reads "**Solved** in 24 moves".
11. 700ms later the result card rises from the bottom (520ms iOS sheet curve): "Picture restored" (or "New best time" in green), "Harbour Light, done.", Moves, Time, Best, and **Play again**, which takes focus. Play again deals a new full shuffle.
## Structure
```
390 × 844 (54 safe top, 34 safe bottom, 23px gutters)
┌──────────────────────────────────────┐
│ 54 safe area │
│ Tessel [ ‖ ] │ header 48, pause 44×44
│ Nº 12 · Harbour Light │
│ ════════════════════════════════════ │ 1px ink rule
│ MOVES │ TIME │ BEST │ hud: 3 cols, 26px numbers
│ 0 │ 0:00 │ 1:58 │
│ ──────────────────────────────────── │ 1px line rule
│ ┌────┐ Harbour Light │ progress card, 56px thumbnail
│ │pic │ 0 of 15 tiles in place │
│ └────┘ ▭▭▭▭▭▭▭▭▭▭▭▭▭▭▭ │ 15 segments × 6px
│ (flex space) │
… 180 more lines. Copy or download for the full brief. Loading the source…Brief
220 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 complete phone mini-game: Tessel, a sliding number puzzle where fifteen tiles each carry a slice of an original flat SVG picture (Harbour Light: a striped lighthouse on a cliff, a low sun, a sailboat on a dusk sea) and their number. Stone paper ground, ink frame, one green for “in place”, one monospace family for everything. The layout follows iOS 26 conventions without glass: a quiet header, a hairline HUD, the board pushed down into the thumb zone, two 56px buttons under it. The details worth copying are the spring snap (tiles overshoot by a hair and settle in 260ms), number chips that turn green when a tile is home, and a 15-segment progress bar that fills as you go, so a hard puzzle always shows you how close you are.
Reference behaviour
- Initial state: a full shuffle (220 random legal moves from solved, so it is always solvable), MOVES 0, TIME 0:00, BEST 1:58. One tile next to the gap has a 3px cream ring and nudges 8px towards the gap every 1.4s. The line under the board reads “Slide a tile into the gap”.
- Tap a tile in the same row or column as the gap: it and every tile between it and the gap slide one cell towards the gap together. Each tile moved adds 1 to MOVES. Taps on tiles not in line with the gap do nothing.
- Swipe on the board (24px or more; the larger axis wins): the tile on the far side of the gap moves into it in the swipe direction. A swipe never also counts as a tap.
- Keyboard: ArrowLeft moves the tile right of the gap left, ArrowRight the tile left of the gap right, ArrowUp the tile below up, ArrowDown the tile above down. Space pauses, P toggles peek, Escape resumes.
- The first move starts the timer and removes the hint; the line becomes “Tap, swipe or use the arrow keys”.
- After every move: each tile in its home cell shows a green number chip; the progress card reads “N of 15 tiles in place” and N of its 15 segments turn green.
- Peek (toggle,
aria-pressed): the solved picture covers the board, labelled “Solved picture”, fading in over 200ms. Tap again to hide. The timer keeps running. - Pause (top-right, 44px): an ink panel covers the board so the picture cannot be studied, with Resume, Restart this shuffle and New easy shuffle (24 moves). The timer stops; Resume or Escape continues it.
- Shuffle deals a new full shuffle and resets moves and time.
- Solving: the hidden 16th tile fades in to complete the picture, every tile pulses to 1.07 in a diagonal ripple (55ms per step from top-left), number chips fade away after 600ms, and the line reads “Solved in 24 moves”.
- 700ms later the result card rises from the bottom (520ms iOS sheet curve): “Picture restored” (or “New best time” in green), “Harbour Light, done.”, Moves, Time, Best, and Play again, which takes focus. Play again deals a new full shuffle.
Structure
390 × 844 (54 safe top, 34 safe bottom, 23px gutters)
┌──────────────────────────────────────┐
│ 54 safe area │
│ Tessel [ ‖ ] │ header 48, pause 44×44
│ Nº 12 · Harbour Light │
│ ════════════════════════════════════ │ 1px ink rule
│ MOVES │ TIME │ BEST │ hud: 3 cols, 26px numbers
│ 0 │ 0:00 │ 1:58 │
│ ──────────────────────────────────── │ 1px line rule
│ ┌────┐ Harbour Light │ progress card, 56px thumbnail
│ │pic │ 0 of 15 tiles in place │
│ └────┘ ▭▭▭▭▭▭▭▭▭▭▭▭▭▭▭ │ 15 segments × 6px
│ (flex space) │
│ ┌──────────────────────────────────┐ │
│ │ ┌──┐┌──┐┌──┐┌──┐ │ │ board 344×344, ink frame, r18, pad 6
│ │ │ 7││11││ ││ 6│ tiles 80×80 │ │ pitch 83 (3px gutters)
│ │ └──┘└──┘└──┘└──┘ │ │
│ │ … 4 rows … │ │
│ └──────────────────────────────────┘ │
│ Slide a tile into the gap │ note, 13px
│ ┌────────────────┐ ┌───────────────┐ │
│ │ ◉ Peek │ │ ↻ Shuffle │ │ 56px buttons, 10px gap
│ └────────────────┘ └───────────────┘ │
└──────────────────────────────────────┘
Result card: left/right 12, bottom 34, r22, slides over the controls and the board's lower half.
<main class="app">flex column,max-width:430px,padding:54px 23px 34px.<header>:<h1>Tessel</h1>, sub-line, pause<button aria-label="Pause">.<section class="hud">: three cells, the time cellrole="timer".<section class="prog">: an<svg><use href="#pic"/></svg>thumbnail, the title, the count text, 15<i>segments..boardwrap(344×344) holds.board[role=group]with.cells, the.peekoverlay and the.pausedrole="dialog"panel.- Tiles: 16
<button class="t">(the 16th isaria-hiddenand invisible until solved). Each holds<svg viewBox="col*83 row*83 80 80"><use href="#pic"/></svg>plus an<i>number chip. .controls: Peek and Shuffle buttons..resultcard witharia-live="polite".- The picture is one
<g id="pic">(332×332) in a hidden<svg><defs>with two linear gradients (sky, sea).
Tokens
:root {
/* colour */
--bg: #E9E4D8; /* stone paper */
--card: #F4F0E6; /* buttons, progress card, result card */
--ink: #1E1E24; /* text, frame, chips, primary button */
--ink-2: #55535C; /* labels (5.9:1 on --bg) */
--line: #CFC8B8; /* hairlines, empty segments */
--frame: #1E1E24; /* board */
--home: #276B4D; /* in-place chip, filled segments, result tag */
--accent: #C8402E; /* focus ring; lighthouse stripes in the picture */
--chip: #F4EFE6; /* chip text, hint ring, paused panel button */
/* picture */
--sky-top: #F6C77A; --sky-bottom: #EE8A5B; --sun: #FCE6AE; --hill: #B5604E; --hill-2: #8F4A44;
--sea-top: #2E5F80; --sea-bottom: #173A52; --glint: #F3B276; --wave: #4A7EA0; --cliff: #2E2C2A; --grass: #5E7A44;
/* type */
--mono: "Space Mono", ui-monospace, monospace;
--fs-title: 22px; --fs-hud: 26px; --fs-result: 24px; --fs-result-num: 22px;
--fs-button: 15px; --fs-body: 13px; --fs-chip: 12px; --fs-label: 11px;
/* board geometry */
--board: 344px; --board-pad: 6px; --pitch: 83px; --tile: 80px;
/* radii */
--r-board: 18px; --r-tile: 9px; --r-chip: 5px; --r-button: 14px; --r-icon: 12px; --r-result: 22px;
/* shadow */
--board-shadow: 0 18px 30px -18px rgba(30,30,36,.6);
/* motion */
--spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--std: cubic-bezier(0.2, 0.7, 0.2, 1);
--sheet: cubic-bezier(0.32, 0.72, 0, 1);
--t-snap: 260ms; --t-ripple: 520ms; --ripple-step: 55ms; --t-overlay: 200ms; --t-result: 520ms; --result-delay: 700ms;
}
Typography
One family, Space Mono, at two weights.
| Role | Size / line | Weight | Tracking | Case |
|---|---|---|---|---|
| Title “Tessel” | 22px / 1 | 700 | −0.02em | sentence |
| HUD numbers | 26px / 1.1, tabular | 700 | 0 | — |
| HUD labels | 11px | 400 | 0.14em | upper |
| Paused heading | 28px / 1 | 700 | 0 | sentence |
| Result heading | 24px / 1.1 | 700 | 0 | sentence |
| Result numbers | 22px / 1.2 | 700 | 0 | — |
| Buttons | 15px / 1 | 700 | 0 | sentence |
| Tile chip | 12px / 20px | 700 | 0 | — |
| Body, note, sub-line | 12–13px / 1.4 | 400 | 0 | sentence |
Motion
| What | Trigger | Property | From → to | Duration | Easing | Delay | Reduced motion |
|---|---|---|---|---|---|---|---|
| Tile snap | move | transform translate | old cell → new cell (slight overshoot) | 260ms | --spring | 0 | 90ms --std, no overshoot |
| Hint nudge | first frame | translate | 0 → 8px towards gap → 0 | 1400ms loop | --std | — | ring only, no nudge |
| Chip home | move | chip background | ink → green | 200ms | default | 0 | same |
| Segments | move | background | line → green | 200ms | default | 0 | same |
| Peek | toggle | opacity | 0 → 1 | 200ms | --std | 0 | same |
| Pause panel | toggle | opacity | 0 → 1 | 200ms | --std | 0 | same |
| Last tile | solve | opacity | 0 → 1 | 500ms | --std | 0 | same |
| Ripple | solve | scale | 1 → 1.07 → 1 | 520ms | --spring | (row + col) × 55ms | off |
| Chips out | solve | opacity | 1 → 0 | 300ms | default | 600ms | same |
| Result card | solve | translateY | 120% → 0 | 520ms | --sheet | 700ms | 200ms fade |
| Button press | :active | scale | 1 → 0.97 | 120ms | --std | 0 | same |
The timer runs on requestAnimationFrame, adds min(100, dt) per frame, and stops requesting frames when paused or solved.
States
- Ready: board visible, hint ring on one movable tile, timer at 0:00 and not running.
- Running: timer counting, note shows the controls line.
- Tile in place: green chip; out of place: ink chip. Both 4.5:1+ with cream text.
- Peeking: Peek button inverted (ink fill, cream text),
aria-pressed="true". - Paused: ink panel over the board, pause button’s label flips to “Resume”, timer frozen. Moves are ignored.
- Solved: 16th tile shown, ripple, chips gone, input ignored, result card up.
- New best: result tag reads “New best time”, HUD BEST updates.
- Focus-visible: 3px
--accentoutline at 3px offset on every button. - Hidden layers (peek, pause, result) are
visibility:hiddenwhen closed, not only transparent or translated.
Accessibility
- The board is
role="group"labelled “Sliding puzzle, 4 by 4”. Tiles are buttons withtabindex="-1"and labels like “Tile 7” or “Tile 1, in place”, so the screen-reader user plays with the arrow keys instead of tabbing through 15 stops. - Tab order: Pause, Peek, Shuffle; inside the paused panel focus goes to Resume; on solve, focus moves to Play again.
- Keys: arrows move, Space pauses, P peeks, Escape resumes.
- A polite live region announces “Paused. Timer stopped at 0:42.” and “Resumed.”; the result card is itself
aria-live. - Contrast:
--ink-2on--bg5.9:1; cream on--home5.5:1; cream on ink 15:1. - Hit targets: tiles 80×80, pause 44×44, buttons 56px tall.
- The board has
touch-action:noneso a swipe never scrolls the page.
Responsive rules
- At 360 wide: drop
--pitchto 75px and--tileto 72px (board 312px); the HUD keeps three columns. - Shorter than 800: the app keeps
min-height:800px; the progress card’smargin-bottom:autoabsorbs any slack, so on tall phones the board sits lower, nearer the thumb. - Tablet: the column caps at 430px and centres; the board does not grow past 344px.
Acceptance checklist
Always
- Shuffle by legal random moves from solved; never by permuting the array.
- Tapping a tile in line with the gap slides the whole run; moves count every tile moved.
- Swipe (24px threshold) and all four arrow keys work, with the directions in step 4.
- Tiles snap with a spring overshoot in 260ms; reduced motion uses 90ms without overshoot.
- Each tile shows its own slice of one picture plus its number; chips turn green in place.
- The progress bar has 15 segments and matches the in-place count.
- Timer is rAF-driven, capped at 100ms per frame, starts on the first move, stops on pause and solve.
- Peek, pause, restart, new easy shuffle and Play again all work and reset what they should.
- Solve shows the 16th tile, a diagonal ripple, then the result card with focus on Play again.
- Closed overlays are
visibility:hidden;scrollWidthstays 390.
This demo
- Title “Tessel”, sub-line “Nº 12 · Harbour Light”, BEST starts at 1:58.
- Picture: lighthouse with two red bands on a left cliff, sun at (236,150) r46, sailboat at the lower right.
- Easy shuffle is 24 moves; full shuffle is 220.
Implementation notes
- One picture, sixteen windows. Draw the scene once as a
<g>in<defs>, then give each tile its ownviewBoxinto it. The 3px gutters are simply never shown, which reads as a cut picture.
const c = (k - 1) % 4, r = Math.floor((k - 1) / 4);
tile.innerHTML = `<svg viewBox="${c * 83} ${r * 83} 80 80" aria-hidden="true"><use href="#pic"/></svg><i>${k}</i>`;
tile.style.setProperty('--x', cell % 4); tile.style.setProperty('--y', Math.floor(cell / 4));
.t { transform: translate(calc(var(--x) * 83px), calc(var(--y) * 83px));
transition: transform 260ms cubic-bezier(0.34, 1.56, 0.64, 1); }
- Sliding a run. Walk from the gap towards the tapped cell, swapping as you go. This handles one tile and three tiles with the same code.
function tap(cell) {
let g = pos.indexOf(0); const gr = g >> 2, gc = g & 3, cr = cell >> 2, cc = cell & 3;
if (cell === g || (gr !== cr && gc !== cc)) return;
const step = gr === cr ? (cc < gc ? -1 : 1) : (cr < gr ? -4 : 4);
while (g !== cell) { const n = g + step; pos[g] = pos[n]; pos[n] = 0; g = n; moves++; }
}
- A timer that cannot run away. Use rAF with a capped delta, and only request the next frame while running. A background tab returns with one 100ms step, not a 40-second jump.
function loop(t) {
const dt = Math.min(100, t - last); last = t; ms += dt; timerEl.textContent = fmt(ms);
if (running && !paused && !won) raf = requestAnimationFrame(loop);
}
// on resume: last = performance.now(); raf = requestAnimationFrame(loop);
Common mistakes: random permutations (half are unsolvable), swipe handlers that also fire the tile’s click, and a pause panel that is transparent instead of opaque, which lets players study the board for free.
Details
- Palette
- Type
- Space Mono
- Motion
- Subtle motion
- Build
- Multi-step
- Tags