Nº 145 of 520
Split map contact form
A two-column contact block: a stylised SVG city map whose studio switcher pans to each pin, beside a floating-label form with simulated send.
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 "Split map contact form" (piece contact-split-map-form) 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º 145 · "Split map contact form" · www.designlounge.live -->
# Split map contact form
> **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 contact block of a small design studio ("Halden Studio") with three addresses in one city. The left side is a hand-drawn-feeling SVG city map built from two rotated block patterns, a channel of water, parks and white arterial roads with grey casing. The right side has a 76px headline, "Come by, or write.", and a four-field form with floating labels. A pill segmented control over the map switches studios. The map pans over 900ms to centre the chosen pin, and the address card and the form's "This one goes to…" line both update. The detail worth copying is how the map and the form are tied together: picking a studio changes who the message is addressed to, which the success state confirms by name.
## Reference behaviour
1. Initial state: "Harbour" is selected. The map is already centred on the Harbour pin (no pan on load). That pin is vermilion, scaled 1.35×, with a slow pulse ring. The address card bottom-left reads "Studio 01 · HQ / Kade 14, Harbour". Company and message fields are prefilled so the floating labels show both states at once.
2. Click "Old Town" in the switcher: the selected pill becomes ink-filled, the map world translates over 900ms with `cubic-bezier(.65,0,.15,1)` until the Old Town pin sits at 58% x / 44% y of the map panel. The previous pin goes grey and the new pin turns vermilion and gains the pulse.
3. During the pan, the card's text fades out over 200ms, swaps at t=200ms, then fades back in. The coordinates label in the bottom-right and the lead line ("This one goes to the Old Town print lab") update at the same moment.
4. Clicking a pin on the map selects that studio, the same as the tab.
5. Arrow Left / Right on the focused tab moves to the previous / next studio (wraps) and moves focus.
6. Focusing an empty field: border turns ink and gains a 4px `rgba(29,35,32,.07)` ring. The label rises 10px and scales to 0.75 over 200ms.
7. Submit with an empty name or an invalid email: the failing field gets a vermilion border and a 4px `--accent-soft` ring, its label turns vermilion, focus moves to the first failing field, and the note beside the button reads "Add your name and a valid email". Typing in a failing field clears its error.
8. Valid submit: the button shows "Sending", turns `--ink-2`, and its arrow spins. After 1400ms the success panel fades and rises 12px into place over the form column: a tick in a soft vermilion disc, "Message received.", and copy naming the studio contact (Ines, Pieter or Saskia).
9. "Send another" hides the success panel, resets the form and focuses the name field.
## Structure
```
1280 × 800
┌──────────────────────────────────────────┬──────────────────────────────┐
│ ( Harbour | Old Town | North Yards ) │ HALDEN STUDIO CONTACT / 03 │ 56 top pad
│ │ │
│ ▒▒ blocks ▒▒ park │ Come by, │ 76px
│ ════ arterial road ═══════ │ or write. │
│ ◉ Harbour (pulse) │ lead 16px, 400px max │
│ ~~~~~~~~~ channel ~~~~~~~~~~~~~~ │ ┌─name────┐ ┌─email─────┐ │ 56px fields
│ │ ┌─company────────────────┐ │
│ ┌ address card 300w ┐ │ ┌─message (116h)─────────┐ │
│ │ STUDIO 01 · HQ │ │ └────────────────────────┘ │
│ │ Kade 14, Harbour │ 51.91° N … │ [ Send message → ] avg reply │ 52px pill
└──────────────────────────────────────────┴──────────────────────────────┘
fluid (680 at 1280) 600 fixed
```
… 176 more lines. Copy or download for the full brief. Loading the source…Brief
216 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
The contact block of a small design studio (“Halden Studio”) with three addresses in one city. The left side is a hand-drawn-feeling SVG city map built from two rotated block patterns, a channel of water, parks and white arterial roads with grey casing. The right side has a 76px headline, “Come by, or write.”, and a four-field form with floating labels. A pill segmented control over the map switches studios. The map pans over 900ms to centre the chosen pin, and the address card and the form’s “This one goes to…” line both update. The detail worth copying is how the map and the form are tied together: picking a studio changes who the message is addressed to, which the success state confirms by name.
Reference behaviour
- Initial state: “Harbour” is selected. The map is already centred on the Harbour pin (no pan on load). That pin is vermilion, scaled 1.35×, with a slow pulse ring. The address card bottom-left reads “Studio 01 · HQ / Kade 14, Harbour”. Company and message fields are prefilled so the floating labels show both states at once.
- Click “Old Town” in the switcher: the selected pill becomes ink-filled, the map world translates over 900ms with
cubic-bezier(.65,0,.15,1)until the Old Town pin sits at 58% x / 44% y of the map panel. The previous pin goes grey and the new pin turns vermilion and gains the pulse. - During the pan, the card’s text fades out over 200ms, swaps at t=200ms, then fades back in. The coordinates label in the bottom-right and the lead line (“This one goes to the Old Town print lab”) update at the same moment.
- Clicking a pin on the map selects that studio, the same as the tab.
- Arrow Left / Right on the focused tab moves to the previous / next studio (wraps) and moves focus.
- Focusing an empty field: border turns ink and gains a 4px
rgba(29,35,32,.07)ring. The label rises 10px and scales to 0.75 over 200ms. - Submit with an empty name or an invalid email: the failing field gets a vermilion border and a 4px
--accent-softring, its label turns vermilion, focus moves to the first failing field, and the note beside the button reads “Add your name and a valid email”. Typing in a failing field clears its error. - Valid submit: the button shows “Sending”, turns
--ink-2, and its arrow spins. After 1400ms the success panel fades and rises 12px into place over the form column: a tick in a soft vermilion disc, “Message received.”, and copy naming the studio contact (Ines, Pieter or Saskia). - “Send another” hides the success panel, resets the form and focuses the name field.
Structure
1280 × 800
┌──────────────────────────────────────────┬──────────────────────────────┐
│ ( Harbour | Old Town | North Yards ) │ HALDEN STUDIO CONTACT / 03 │ 56 top pad
│ │ │
│ ▒▒ blocks ▒▒ park │ Come by, │ 76px
│ ════ arterial road ═══════ │ or write. │
│ ◉ Harbour (pulse) │ lead 16px, 400px max │
│ ~~~~~~~~~ channel ~~~~~~~~~~~~~~ │ ┌─name────┐ ┌─email─────┐ │ 56px fields
│ │ ┌─company────────────────┐ │
│ ┌ address card 300w ┐ │ ┌─message (116h)─────────┐ │
│ │ STUDIO 01 · HQ │ │ └────────────────────────┘ │
│ │ Kade 14, Harbour │ 51.91° N … │ [ Send message → ] avg reply │ 52px pill
└──────────────────────────────────────────┴──────────────────────────────┘
fluid (680 at 1280) 600 fixed
<body>is a two-column grid:minmax(0,1fr) 600px.<section class="map" aria-label="Studio locations">,overflow:hidden..worldis a 1600 × 1300 absolutely positioned layer containing one inline<svg>and three.pindivs. Only this layer is transformed. It isaria-hidden; the card carries the real content..switch[role=tablist]at 32/32, three<button role=tab>..card[aria-live=polite]at left 32, bottom 32, 300px wide: tag,<h3>name, address,<dl>hours + phone..coordsbottom-right, mono 11px.
<section class="side">with 56/64/48 padding: eyebrow row,<h1>,.lead,<form novalidate>on a 2-column grid with 12px gap (name + email share a row; company and message span both), action row..done[role=status]absolutely covers the side column for the success state.
Tokens
:root {
/* map */
--land: #e4e8e1; /* street colour between blocks */
--block: #d9ded5; /* building blocks */
--water: #c3d5da;
--water-edge: #adc4cb; /* 2px shoreline stroke */
--park: #cfdcc4;
--road: #fbfcfa; /* arterial fill */
--road-edge: #cdd3ca; /* arterial casing, 4–5px wider than the fill */
/* surfaces + text */
--bg: #fafaf7;
--field: #ffffff;
--line: #e1e3dc;
--line-2: #c9cdc3; /* field border */
--ink: #1d2320;
--ink-2: #5b635d;
--ink-3: #8a918b; /* placeholders/labels at rest */
--accent: #e2553b; /* active pin, headline word, errors */
--accent-soft: #fbe4de;
--ok: #2f7a57;
/* type */
--sans: "Hanken Grotesk", system-ui, sans-serif;
--mono: "DM Mono", ui-monospace, monospace;
/* shape */
--r: 10px; /* fields */
--r-lg: 18px; /* address card */
--field-h: 56px;
--shadow-card: 0 1px 0 rgba(29,35,32,.04), 0 12px 32px -12px rgba(29,35,32,.22);
/* motion */
--t-micro: 160ms;
--t-label: 200ms;
--t-pan: 900ms;
--ease: cubic-bezier(.2,.7,.2,1);
--ease-pan: cubic-bezier(.65,0,.15,1);
--ease-out: cubic-bezier(.16,1,.3,1);
}
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Headline | Hanken Grotesk | 76px | 600 | 0.94 | −0.045em | sentence |
| Success title | Hanken Grotesk | 48px | 600 | 1 | −0.04em | sentence |
| Card name | Hanken Grotesk | 22px | 600 | 1.3 | −0.02em | sentence |
| Lead | Hanken Grotesk | 16px | 400 | 1.5 | 0 | sentence |
| Field value/label | Hanken Grotesk | 15px | 400 | 1.45 | 0 | sentence |
| Button | Hanken Grotesk | 15px | 600 | 1 | 0 | sentence |
| Tabs | Hanken Grotesk | 13px | 500 | 1 | 0 | sentence |
| Eyebrow / tag | DM Mono | 11–12px | 500 | 1.4 | +0.1em | UPPERCASE |
| Pin label | DM Mono | 11px | 500 | 20px | +0.06em | UPPERCASE |
| Card meta / note | DM Mono | 12px | 400 | 1.6 | 0 | sentence |
The single word “write.” in the headline is --accent; nothing else in the column is coloured.
Motion
| Element | Trigger | Property | From → To | Duration | Easing | Notes |
|---|---|---|---|---|---|---|
.world | studio change | transform | current → centre on new pin | 900ms | --ease-pan | none on first paint (set transform with transition:none, restore after 2 rAFs) |
.pin i | select | background, scale | grey 1 → vermilion 1.35 | 160 / 200ms | --ease / --ease-out | |
.pin.on::before | active pin | scale, opacity | 0.3, .9 → 1.6, 0 | 2400ms | --ease-out | infinite, calm |
| card text | studio change | opacity | 1 → 0 → 1 | 200ms ×2 | --ease | content swaps at 200ms |
| floating label | focus / filled | translateY, scale | 0, 1 → −10px, .75 | 200ms | --ease | transform-origin:left top |
| field ring | focus | border, box-shadow | --line-2 → --ink + 4px ring | 160ms | --ease | |
| send arrow | hover | translateX | 0 → 3px | 200ms | --ease-out | |
| send arrow | busy | rotate | 0 → 360° | 800ms | linear | spinner only |
.done | success | opacity, translateY | 0, 12px → 1, 0 | 400ms | --ease / --ease-out | visibility delayed on hide |
Reduced motion: every transition is 1ms and all keyframe animations are removed (no pulse, no spinner). The pan becomes a jump; the content still updates.
States
- Tab hover: text
--ink-2→--ink. Selected: ink fill,--bgtext,aria-selected="true". - Pin: inactive is a 14px grey disc with a 3px white border. Active is vermilion at 1.35× with a 52px pulse ring, and its label weight and colour go to
--ink. - Field rest: 1px
--line-2, label 15px--ink-3vertically centred (top 18px). - Field focus / filled: label raised (detected with
:placeholder-shownon aplaceholder=" "), border--ink, ring. - Field error:
.badsets the border and label to--accentwith a--accent-soft4px ring, plusaria-invalid="true". - Button hover:
#000; active scale .98; busy usesaria-busy="true",--ink-2fill, spinner, pointer-events off. - Success: a full-column overlay with a 64px tick disc, the title, a sentence naming the studio contact, and a ghost “Send another” button (44px).
Accessibility
- The switcher is a real tablist. Use roving
tabindex(selected = 0, others = −1), and let Arrow Left/Right move and select. - The map layer is
aria-hidden="true". The address card isaria-live="polite", so the new studio’s name and address are announced after a switch. - Each field has a real
<label for>. The floating label is the visible label, not a placeholder. - Errors set
aria-invalidand focus the first bad field. The note beside the button isaria-live="polite". - The success panel is
role="status", and focus moves to “Send another”. - Focus-visible: a 2px vermilion outline with 2px offset on tabs and buttons. Fields use the ink border + ring.
- Contrast:
--ink-2on--bgis 6.3:1. Field labels at rest (--ink-3) are 3.4:1 and are acceptable only because the value text they sit beside is--ink; raise them to--ink-2if your audit requires 4.5:1 for labels.
Responsive rules
- ≥ 1280: as specified; the map column is fluid and the form column is fixed at 600px.
- 1024–1279: form column 520px, headline 64px, side padding 48px.
- 768–1023: stack the columns. The map is 360px tall on top, and the card moves to the map’s bottom-left at 24px. The form takes the full width with a 640px max.
- < 640: map 280px; the switcher becomes a horizontally scrollable row; name and email stack to one column; headline 48px.
- Re-run the pan on
resize, because pin centring depends on the map panel’s size.
Acceptance checklist
- The body grid is
minmax(0,1fr) 600pxat 1280 × 800, and nothing scrolls. - The map is a single inline SVG with two rotated
<pattern>block grids, water, three parks and cased arterial roads. No raster tiles. - Switching studios translates only the
.worldlayer, over 900ms withcubic-bezier(.65,0,.15,1). - The selected pin ends at 58% / 44% of the map panel.
- No pan animation on first load.
- Card text fades out, swaps at 200ms and fades in. Coordinates and the form’s lead line update too.
- Arrow keys move between tabs with roving tabindex.
- Floating labels rise to −10px at scale .75 on focus and when filled, and never overlap the value.
- Invalid submit marks fields with
aria-invalid, focuses the first one, and updates the live note. - Valid submit shows the busy state for 1400ms, then a success panel naming the selected studio’s contact.
- “Send another” restores the form and focuses the name field.
- Reduced motion removes the pulse and spinner and makes the pan instant.
- Focus is visible on every tab, field and button.
Implementation notes
Pan a fixed-size world, not the SVG viewBox. Animating transform on a wrapper is GPU-friendly and lets the HTML pins ride along with the SVG.
function pan() {
const r = map.getBoundingClientRect(), s = STUDIOS[cur];
world.style.transform =
`translate(${Math.round(r.width * .58 - s.x)}px, ${Math.round(r.height * .44 - s.y)}px)`;
}
world.style.transition = 'none'; pan();
requestAnimationFrame(() => requestAnimationFrame(() => world.style.transition = ''));
City blocks from a pattern. Two patterns, rotated −14° and 22°, give the map an old-district and new-district grain with almost no markup:
<pattern id="grid" width="56" height="44" patternUnits="userSpaceOnUse" patternTransform="rotate(-14)">
<rect width="56" height="44" fill="var(--land)"/>
<rect x="4" y="4" width="48" height="36" rx="3" fill="var(--block)"/>
</pattern>
Draw each arterial twice, a wide --road-edge stroke and then a narrower --road stroke on the same path. That gives the road its casing.
Floating labels without JS. Give every input placeholder=" " and key off :placeholder-shown:
.f input:focus + label,
.f input:not(:placeholder-shown) + label { transform: translateY(-10px) scale(.75); color: var(--ink-2); }
.f input { padding: 24px 16px 8px; height: 56px; }
Common mistakes:
- Putting the label before the input, which breaks the sibling selector.
- Animating
topinstead oftransform. - Forgetting to re-pan on resize.
- Letting the address card live inside the transformed world, which makes it slide away with the map.
Details
- Palette
- Type
- Hanken Grotesk · DM Mono
- Motion
- Subtle motion
- Build
- One session
- Tags
Studied
Don't Board MeA service with a next step. The headline can shout. The booking stays plain.
All sources