Design Lounge
Install

Nº 578 of 605

Adventure quest map

An island world map in SVG with quest nodes that are done, active, open or locked, a dotted trail that draws to the chosen quest, and a quest panel.

Preview

Open full size
1280 × 800 · 19.1 KB · 187 lines

Use it

Free to use · credit appreciated

1 Once, in your project · Node 22.20+

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Adventure quest map" (piece game-quest-map) 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.

Brief

179 lines

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

The world map of an island adventure game, drawn flat like a modern indie board game: a teal sea with a tiled wave pattern, islands with a wide shallow-water ring, sand, two greens, a range of terracotta peaks with snow caps, forest dots, a river, villages and beacon markers. Eight quest nodes sit on the map in four states (done, active, available, locked). Choosing one draws a dotted trail from your camp to it and fills the right-hand quest panel with its objectives, progress, rewards and next step. The UI chrome is chunky: 2px ink outlines and 4px offset shadows, so it reads as cut-out cardboard over the painted map. The detail worth copying: the dotted trail is revealed through an SVG mask whose stroke animates, so dots stay round and evenly spaced while the trail “walks” out.

Reference behaviour

  1. Initial state: “Beacons of Thornvale” (main story, active) is selected. Its node has a 24px ink ring, a coral pulse ring (2.4s loop), and a cream name tag beside it. A dotted trail runs from the camp tent near Saltmere to the node.
  2. Hover a node: its disc grows from r 16 to 19 over 200ms.
  3. Click a node, or focus it and press Enter or Space: the ring and name tag move to it, the trail redraws from the camp over 900ms, and the panel swaps to that quest.
  4. The panel shows: type pill and state pill, title, region · level · giver, description, objectives (done items ticked and struck through; counted items show “1/3” and a 6px coral bar), rewards (XP, silver, an item in gold), a dark Next step callout when there is one, then the primary button and distance (“3.1 km from camp, about 37 min on foot”).
  5. The primary button reads “Tracking” for the active quest, “Track quest” for available ones, “Completed” (disabled) for done, “Locked” (disabled) for locked. A locked quest replaces objectives with “Requires: Beacons of Thornvale” on a tinted strip, and the finale hides its rewards as ”?”.
  6. The filter pill (All, Main story, Side) fades non-matching nodes to opacity .25 and removes them from the tab order and pointer hits; the map never re-lays out.
  7. Only the active node pulses. Nothing else loops.

Structure

1280 × 800
┌───────────────────────────── map 1fr (SVG 900 × 800, slice) ─────────┬── panel 380 ──┐
│ ┌ The Wayfarer Isles ┐                          (All|Main story|Side)│ [MAIN][ACTIVE]│
│ └ Day 23 · 5 of 8 ───┘                                               │ Beacons of    │
│  THE GLASS SEA     SALTMERE ✓     EMBERREACH  ▲▲ 🔒                   │ Thornvale 32px│
│           ✓   ⌂⌂   ~~~river~~~                  🔒                    │ meta · desc   │
│  (N)    ⛺ ·  ·  ·                ○        ▵ ▵ ▵                      │ ─ ─ ─ ─ ─ ─   │
│          MIRROWOOD ○  ·  ·  · (★) [Beacons of Thornvale]              │ OBJECTIVES    │
│                     THORNVALE                       ( 🔒 )            │ ☑ ☐ 1/3 ▬ ☐   │
│   ( isle ) · · · · · · · · · · · · · · · · · · · ·( isle )           │ REWARDS 3 up  │
│ [● Active ○ Available ● Done ● Locked]          HOLLOW ISLES          │ NEXT STEP     │
│                                                                       │ [Tracking] 3.1│
└───────────────────────────────────────────────────────────────────────┴───────────────┘
body: grid minmax(0,1fr) | 380px; the panel has a 3px ink left border
  • <section class="map"> holds one <svg viewBox="0 0 900 800" preserveAspectRatio="xMidYMid slice"> and three HTML overlays: title card, filter group, legend.
  • SVG layers in order: sea fill, wave <pattern> (60 × 34) at 60%, shallows (island paths with a 28px --shallow stroke), sand, grass, highland, forest, peaks, river and bridge, region <text>, villages, beacons, sea route, the trail <path id="route" mask>, camp tent, <g id="nodes">, the name tag group, compass.
  • Each node is <g class="node" role="button" tabindex="0" aria-label> with an optional pulse circle, a selection ring, a 16px disc and a glyph (star for main, dot for side, tick for done, padlock for locked).
  • <aside aria-live="polite"> is re-rendered from data on selection.

Tokens

:root {
  /* map */
  --sea: #1f5d66;  --shallow: #4b9aa0;  --wave: #2f7880;
  --sand: #f1d9a7; --grass: #8bb86f;    --grass-2: #6e9f58; --wood: #4f8a52;
  --rock: #c4805e; --rock-2: #9b5f45;   --snow: #fbf3e4;    --river: #7db0b6;
  /* UI */
  --paper: #fdf6e8;   /* cards, panel */
  --paper-2: #f4e9d2; /* progress track, lock strip, disabled */
  --ink: #1f2b33;     /* outlines, text, offset shadows */
  --ink-2: #4c5a63;
  --ink-3: #5e6b73;
  --line: #e2d4b8;    /* dashed section rules */
  /* quest states */
  --active: #c8401a; --done: #3f7d5a; --open: #fdf6e8; --locked: #7d8a90;
  --gold: #8a5a12;    /* item reward */
  /* type */
  --display: "Righteous", sans-serif;
  --text: "Nunito", sans-serif;
  /* shape */
  --outline: 2px solid var(--ink);
  --shadow: 4px 4px 0 var(--ink); /* buttons 3px */
  --r-card: 14px; --r-btn: 12px; --r-reward: 10px;
  /* motion */
  --t-trail: 900ms; --t-pulse: 2.4s; --t-hover: 200ms;
  --ease: cubic-bezier(.2,.7,.2,1);
  --ease-out: cubic-bezier(.16,1,.3,1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
Quest titleRighteousclamp(26px, 2.6vw, 32px)4001.050title
Map titleRighteous26px4001.02emtitle
Region label (SVG)Righteous20px4001.22emupper, 75% opacity
Sea label (SVG)Righteous16px4001.4emupper
Reward valueRighteous20px (item 15px)4001.10
Body, objectivesNunito15px6001.45 to 1.550sentence
Meta, distanceNunito13 to 14px600 to 7001.30
Pills, section headsNunito11 to 12px8001.14 to .16emupper
Node tag (SVG)Nunito13px80010title

Motion

ElementTriggerPropertyFrom → ToDurationEasing
Trail maskselectstroke-dashoffsetpath length → 0900ms--ease-out
Active pulsealwaysscale, opacity.6, .9 → 1.9, 02.4s loop--ease-out
Node dischoverr16 → 19200ms--ease
Buttonshover / activetranslate, shadow0, 3px → −1px, 4px / 2px, 1px120ms--ease
Objective barrenderwidth0 → value500ms--ease-out

Reduced motion: no pulse and the trail appears complete at once; selection and the panel still update.

States

  • Node done: --done disc, white tick. Active: --active disc, white star, pulse. Available: --open disc with ink glyph. Locked: --locked disc, dashed 2.5px outline (4 3), white padlock.
  • Node selected: 24px ink ring at full opacity plus the name tag (cream pill, 1.5px ink stroke, 30px tall), placed right of the node or left when it would leave the map.
  • Node focus-visible: the ring turns --active at 3px.
  • Filtered out: opacity .25, pointer-events:none, tabindex=-1.
  • Filter pressed: ink pill with paper text.
  • Primary button: --active fill, white text, ink outline and 3px shadow; disabled: --paper-2, --ink-3 text, --locked outline, no shadow.
  • Objective done: green filled box with a white tick, text struck in --ink-3.

Accessibility

  • Nodes are focusable SVG groups with role="button" and labels like “Beacons of Thornvale, Active, main story”; Enter and Space select.
  • Filters are a role="group" of buttons with aria-pressed.
  • The panel is aria-live="polite": the new quest title and objectives are read after a selection.
  • The legend is decorative (aria-hidden); state is in each node’s label and in the panel pill, so colour is never the only cue.
  • Contrast on --paper: --ink 13.4:1, --ink-2 6.6:1, --ink-3 5.1:1, --gold 5.5:1; white on --active 5.0:1 (the brighter #d9481f only reaches 4.3:1, so it is not used under text).

Responsive rules

  • ≥ 1280: map and 380px panel side by side, no page scroll; the SVG slices so wider screens show more sea.
  • 1024 to 1279: the panel stays 380px; the map crops at the sides.
  • 900 and below: one column. The map is 480px tall (still slice, so it crops to the island centre), the panel follows under a 3px top border and the page scrolls. The title card and filters stack top-left at 12px; the legend spans the bottom. Labels and the compass that sit on the cropped edges (.edge) are hidden so no text is cut by the frame. Rewards become two columns with the item full width.

Acceptance checklist

Always

  • Four node states are distinct by fill, glyph and outline style, not colour alone.
  • Exactly one node pulses: the active quest.
  • Selecting a node draws a dotted trail from the camp over 900ms through a mask, with round, evenly spaced dots.
  • The trail is a quadratic curve that bows to one side, never a straight line.
  • The panel swaps in place and announces politely; locked quests show their requirement instead of objectives.
  • Counted objectives show “n/m” and a progress bar.
  • One filled primary button whose label matches the quest state; done and locked disable it.
  • Filtering dims and de-focuses nodes without moving anything.
  • At 390 wide nothing scrolls sideways and no map text is cut by the frame.

This demo

  • Eight quests: 2 done, 1 active, 2 available, 3 locked; title card “Day 23 · 5 of 8 quests open”.
  • First frame: Beacons of Thornvale, Level 12, from Captain Rook Ashby, beacons 1/3, rewards 1,200 XP, 340 silver, Wayfarer’s Compass.
  • Distance reads 3.1 km from camp, about 37 min on foot.
  • Fonts are Righteous and Nunito only.

Implementation notes

Shallows for free. Draw each island twice: once with a wide stroke in the shallow colour, then the fill on top. The stroke becomes a coastline ring that follows every curve.

<g stroke="var(--shallow)" stroke-width="28" stroke-linejoin="round" fill="var(--shallow)"><use href="#isle"/></g>
<g fill="var(--sand)"><use href="#isle"/></g>

A dotted trail that draws. Animating stroke-dashoffset on a dotted path slides the dots. Keep the dots static and reveal them through a mask whose solid stroke draws instead:

#route{fill:none;stroke:var(--paper);stroke-width:4;stroke-linecap:round;stroke-dasharray:.1 11}
#routeMask{fill:none;stroke:#fff;stroke-width:12}
.drawing{animation:draw 900ms var(--ease-out) both}
@keyframes draw{from{stroke-dashoffset:var(--len)}to{stroke-dashoffset:0}}
const d = `M${x0} ${y0}Q${mx - dy * .25} ${my + dx * .25} ${x1} ${y1}`;
route.setAttribute('d', d); mask.setAttribute('d', d);
const len = mask.getTotalLength();
mask.style.setProperty('--len', len); mask.style.strokeDasharray = len;
mask.classList.remove('drawing'); mask.getBoundingClientRect(); mask.classList.add('drawing');

Never nest a bare <svg> icon inside the map. An inner <svg> without width and height renders at 300 by 150 user units; inside a scaled map that is a giant glyph. Inline the <path> and position it with a transform.

Common mistakes: map text that runs off the cropped edge on phones; region labels placed over nodes; a drop shadow on map shapes (the offset ink shadow belongs only to UI chrome).

Details

Palette
Type
Righteous · Nunito
Motion
Subtle motion
Build
Multi-step
Designed bySusan AcharyaKathmandu

Studied

Linear

A control. It studies this site for quiet, not for a show.

All sources