Nº 610 of 613
Mailbox flag newsletter signup
Letter-club signup beside a hand-drawn country mailbox: the door cracks open as you type, the red flag rises on a valid email, and a lilac envelope is posted on submit.
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 "Mailbox flag newsletter signup" (piece newsletter-mailbox-flag) 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º 610 · "Mailbox flag newsletter signup" · www.designlounge.live -->
# Mailbox flag newsletter signup
> **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 signup section of The Hedgerow Post, an invented letter club that pairs strangers as pen pals once a month. On the left, a hand-drawn galvanised country mailbox stands on a wooden post in a small meadow patch, drawn in inline SVG with wobbly ink lines (a turbulence displacement filter), hatching and flat fills on grainy warm paper. On the right, a Petrona headline, a short pitch, an email form and three handwritten facts. The mailbox follows the form: its door opens a crack while you type, its red flag swings up the moment the address is valid, and submitting folds a lilac envelope, posts it through the door and shows a thank-you note with the address used. The detail worth copying is that every form state has a physical twin in the drawing, so the form feels like it is talking back.
## Reference behaviour
1. First frame: door closed, flag down (arm horizontal along the box side), a robin sitting on the box, envelope invisible. The input is empty with the placeholder `you@yourlane.co.uk`; the helper line reads "One email on the first of each month. Never shared, never sold."
2. Type any character: the SVG gets `.typing` and the door scales to `scaleY(.84)` from its bottom hinge in 320ms expo-out, so a dark slit of the box interior shows above it. Clearing the field closes it again.
3. When the trimmed value matches `/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/`, the SVG gets `.up` and the flag rotates 90° about its rivet (420, 262) in 520ms with a spring curve, so the plate stands above the box. Making the value invalid again lowers it.
4. Submit empty: helper turns red with an info-circle icon, copy "Add your email so we can send the first prompt."; `aria-invalid="true"`; focus returns to the input; the flag wobbles (rotate 0 → 16° → −7° → 6° → 0 over 620ms). The wobble class is removed on `animationend` so it can replay.
5. Submit an invalid string: same chrome, copy "That address needs an @ and a domain, like ann@yourlane.co.uk."
6. Typing after an error restores the default helper line and `aria-invalid="false"`.
7. Submit a valid address: the SVG gets `.typing .up .sent` and runs a 2400ms sequence (see Motion): envelope fades in above the box, the letter sheet slides down into it, the flap folds shut, the door swings fully open (`scaleY(-.46)`), the envelope flies to the door while shrinking to 36% and fading, the door closes, the robin hops. The flag stays up.
8. At 1900ms the form and facts become `visibility:hidden` and the thank-you note appears in the same grid cell (no layout shift): "Posted. Thank you." / "Your welcome letter is on its way to {address}. Look for a lilac envelope and your first pen pal on Wednesday 4 November." The note receives focus (`preventScroll`).
9. "Use a different address" clears the field, removes `.sent .typing .up`, hides the note, shows the form and focuses the input.
10. Reduced motion: every transition and keyframe runs in 1ms, the note shows immediately on submit, the drawing lands on its end state (door closed, flag up, envelope hidden).
## Structure
```
1280 × 800, paper #F1E8D4 with an SVG-noise grain tile (180px)
section.wrap grid 600px | 1fr, gap 24, pad 40 88 40 48, align center
┌──────────── .art 600 × 643 (SVG viewBox 560×600) ─┐ ┌──── .copy max 520 ─────────────────┐
│ cloud ~ birds ^ ^ │ │ ~ The Hedgerow Post, a letter club │ Mali 15 red
│ │ │ Write to a stranger. │ Petrona 64/500
│ ┌──door──┬───── body (steel) ────────┐ │ │ Get a real letter back. (italic) │
│ │ No. 12 │ [flag]──────o rivet │robin │ │ Once a month we pair you… 19px │
│ └────────┴───────────────────────────┘ │ │ Your email, for the monthly prompt │ Mali 15
│ ══════════ plank ══════════ │ │ [ input 54px, 1.5px ink ][Join the club ✉] │
│ ║ post tree │ │ helper / error line 14px, aria-live│
│ fence ‾‾‾‾‾‾‾‾‾‾ ║ ‾‾‾ far hill (sage) ‾‾‾‾‾‾ │ │ - - - - - - - - - - - - - - - - - -│
│ meadow * flowers║ mound stone * │ │ Wed 4 Nov | 2,418 | Free │ Petrona 21
│ (land patch clipped to a soft hand-cut blob) │ │ next letters | members in | you buy │ Mali 14
└────────────────────────────────────────────────────┘ └────────────────────────────────────┘
… 175 more lines. Copy or download for the full brief. Loading the source…Brief
215 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 signup section of The Hedgerow Post, an invented letter club that pairs strangers as pen pals once a month. On the left, a hand-drawn galvanised country mailbox stands on a wooden post in a small meadow patch, drawn in inline SVG with wobbly ink lines (a turbulence displacement filter), hatching and flat fills on grainy warm paper. On the right, a Petrona headline, a short pitch, an email form and three handwritten facts. The mailbox follows the form: its door opens a crack while you type, its red flag swings up the moment the address is valid, and submitting folds a lilac envelope, posts it through the door and shows a thank-you note with the address used. The detail worth copying is that every form state has a physical twin in the drawing, so the form feels like it is talking back.
Reference behaviour
- First frame: door closed, flag down (arm horizontal along the box side), a robin sitting on the box, envelope invisible. The input is empty with the placeholder
you@yourlane.co.uk; the helper line reads “One email on the first of each month. Never shared, never sold.” - Type any character: the SVG gets
.typingand the door scales toscaleY(.84)from its bottom hinge in 320ms expo-out, so a dark slit of the box interior shows above it. Clearing the field closes it again. - When the trimmed value matches
/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/, the SVG gets.upand the flag rotates 90° about its rivet (420, 262) in 520ms with a spring curve, so the plate stands above the box. Making the value invalid again lowers it. - Submit empty: helper turns red with an info-circle icon, copy “Add your email so we can send the first prompt.”;
aria-invalid="true"; focus returns to the input; the flag wobbles (rotate 0 → 16° → −7° → 6° → 0 over 620ms). The wobble class is removed onanimationendso it can replay. - Submit an invalid string: same chrome, copy “That address needs an @ and a domain, like ann@yourlane.co.uk.”
- Typing after an error restores the default helper line and
aria-invalid="false". - Submit a valid address: the SVG gets
.typing .up .sentand runs a 2400ms sequence (see Motion): envelope fades in above the box, the letter sheet slides down into it, the flap folds shut, the door swings fully open (scaleY(-.46)), the envelope flies to the door while shrinking to 36% and fading, the door closes, the robin hops. The flag stays up. - At 1900ms the form and facts become
visibility:hiddenand the thank-you note appears in the same grid cell (no layout shift): “Posted. Thank you.” / “Your welcome letter is on its way to {address}. Look for a lilac envelope and your first pen pal on Wednesday 4 November.” The note receives focus (preventScroll). - “Use a different address” clears the field, removes
.sent .typing .up, hides the note, shows the form and focuses the input. - Reduced motion: every transition and keyframe runs in 1ms, the note shows immediately on submit, the drawing lands on its end state (door closed, flag up, envelope hidden).
Structure
1280 × 800, paper #F1E8D4 with an SVG-noise grain tile (180px)
section.wrap grid 600px | 1fr, gap 24, pad 40 88 40 48, align center
┌──────────── .art 600 × 643 (SVG viewBox 560×600) ─┐ ┌──── .copy max 520 ─────────────────┐
│ cloud ~ birds ^ ^ │ │ ~ The Hedgerow Post, a letter club │ Mali 15 red
│ │ │ Write to a stranger. │ Petrona 64/500
│ ┌──door──┬───── body (steel) ────────┐ │ │ Get a real letter back. (italic) │
│ │ No. 12 │ [flag]──────o rivet │robin │ │ Once a month we pair you… 19px │
│ └────────┴───────────────────────────┘ │ │ Your email, for the monthly prompt │ Mali 15
│ ══════════ plank ══════════ │ │ [ input 54px, 1.5px ink ][Join the club ✉] │
│ ║ post tree │ │ helper / error line 14px, aria-live│
│ fence ‾‾‾‾‾‾‾‾‾‾ ║ ‾‾‾ far hill (sage) ‾‾‾‾‾‾ │ │ - - - - - - - - - - - - - - - - - -│
│ meadow * flowers║ mound stone * │ │ Wed 4 Nov | 2,418 | Free │ Petrona 21
│ (land patch clipped to a soft hand-cut blob) │ │ next letters | members in | you buy │ Mali 14
└────────────────────────────────────────────────────┘ └────────────────────────────────────┘
<section class="wrap" aria-labelledby="sub-h">holds both columns..art > svg#box[role=img]with a<title>describing the mailbox. Groups: clouds and birds,g[clip-path=url(#land)](far hill, fence, tree, meadow, flowers, mound), post, plank, body, interior,#flag, rivet,#door, robin#bird,#env(back,.letter, front pocket, stamp,.flap). All of it sits insideg[filter=url(#rough)]..copy:p.kick(squiggle SVG + text),h2#sub-hwith<em>,p.lede,.slot(a 1×1 grid) containing#formbox(form +ul.facts) and.note#note, stacked withgrid-area:1/1.- Form: visible
<label for="email">,.rowflex withinput#email[type=email]andbutton.go[type=submit], thenp#msg[aria-live=polite].novalidateon the form. - Note:
div.note[tabindex=-1][aria-live=polite]withh3,pcontainingb#addr, andbutton.again.
Tokens
:root {
/* colour */
--paper: #f1e8d4; /* page ground */
--card: #fbf6ea; /* input fill, note card, letter sheet */
--ink: #2b2620; /* text, every drawn line, input border */
--ink-2: #594f43; /* lede, helper, fact captions */
--line: #cbbd9f; /* dashed facts rule, column rules, note shadow */
--red: #b3361f; /* flag, button, kicker, error, stamp */
--red-ink: #fff6ea; /* button label */
--steel: #aebcbd; /* mailbox body */
--steel-2: #c7d2d1; /* mailbox door */
--wood: #b98d5c; /* post; plank is #a37a4c */
--sage: #cdd2a9; /* far hill */
--meadow: #b0bd88; /* near meadow */
--lilac: #d8cce6; /* envelope front, flowers */
--interior: #2f2a24; /* inside of the box */
/* type */
--serif: "Petrona", Georgia, serif;
--hand: "Mali", ui-rounded, cursive;
--fs-h2: 64px; --fs-lede: 19px; --fs-input: 18px; --fs-label: 15px; --fs-note: 14px;
/* shape and space */
--r: 6px; --h: 54px;
--sp-1: 8px; --sp-2: 16px; --sp-3: 24px; --sp-4: 32px; --sp-5: 48px;
--shadow-btn: 0 2px 0 #6f1f10;
--shadow-note: 4px 5px 0 var(--line);
/* motion */
--t-door: 320ms; --t-flag: 520ms; --t-send: 2400ms; --t-micro: 150ms;
--ease: cubic-bezier(.2,.7,.2,1);
--ease-out: cubic-bezier(.16,1,.3,1);
--spring: cubic-bezier(.34,1.56,.64,1);
}
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Kicker | Mali | 15px | 500 | 1.3 | 0 | sentence |
| Headline | Petrona | 64px | 500, em 400 italic | 1.02 | −0.022em | sentence |
| Lede | Petrona | 19px | 400 | 1.55 | 0 | sentence |
| Label | Mali | 15px | 500 | 1 | 0 | sentence |
| Input | Petrona | 18px | 400 | 54px box | 0 | as typed |
| Button | Mali | 16px | 600 | 54px box | 0 | sentence |
| Helper / error | Mali | 14px | 400 | 1.5 | 0 | sentence |
| Fact value | Petrona | 21px | 500 | 1.2 | 0 | sentence |
| Fact caption | Mali | 14px | 400 | 1.4 | 0 | sentence |
| Note title | Mali | 22px | 600 | 1.25 | 0 | sentence |
| Note body | Petrona | 17px | 400, address 600 | 1.5 | 0 | sentence |
| Door number (SVG) | Mali | 15px | 600 | n/a | 0 | “No. 12” |
Motion
| Element | Trigger | Property | From → To | Duration | Easing | Notes |
|---|---|---|---|---|---|---|
| Door | input non-empty | transform | scaleY(1) → scaleY(.84), origin 200 316 | 320ms | --ease-out | reverses when emptied |
| Flag | value valid | transform | rotate(0) → rotate(90deg), origin 420 262 | 520ms | --spring | overshoots a little |
| Flag | invalid submit | rotate keyframes | 0 → 16° → −7° → 6° → 0 | 620ms | --ease | class removed on animationend |
| Envelope | submit | opacity, transform | 0%: 0, y 92 · 12%: 1, y 104 · 50%: hold · 74%: (196,262) scale .5 rotate 8° · 80–100%: 0, (204,272) scale .36 | 2400ms | --ease | one keyframe track |
| Letter sheet | submit | translateY | 0–14%: 0 → 32%: 44px | 2400ms | --ease | hides behind the front pocket |
| Flap | submit | scaleY about y −39 | 0–34%: 1 → 50%: −1 | 2400ms | --ease | folds over the pocket |
| Door | submit | scaleY | 0–38%: .84 → 52–78%: −.46 → 92–100%: 1 | 2400ms | --ease | number hidden 40–84% |
| Robin | submit | transform | 82%: none → 88%: translateY(−10) rotate(−6°) → 94%: none | 2400ms | --ease | the robin startles |
| Note | 1900ms after submit | opacity, translateY | 0, 10px → 1, 0 (rotate −.6° kept) | 520ms | --ease-out | |
| Button | hover / active | translateY, shadow | −1px, 3px shadow / +1px, 1px shadow | 150ms | --ease |
Reduced motion: *, svg * { transition-duration: 1ms; animation-duration: 1ms; animation-delay: 0 }; the note appears without the 1900ms wait.
States
- Idle: door closed, flag down, input border 1.5px
--inkon--card, helper--ink-2. - Typing (
.typing): door cracked, dark interior slit visible above it. - Valid (
.up): flag vertical with the red plate above the box. - Input focus:
box-shadow: 0 0 0 3px #b3361f40ring, no outline change on the border. - Error: input
border-color: --red,aria-invalid="true", helper red with a 16px info-circle icon, flag wobble. - Sent (
.sent): 2400ms sequence, then the note replaces form and facts in the same cell. - Button hover / active: lifts 1px / presses 1px; focus-visible is a 2px
--inkoutline, 3px offset. - Reset link hover: colour
--red; underline 1.5px, offset 4px.
Accessibility
- The section is labelled by the headline. The email has a visible
<label>;aria-describedby="msg"links it to the helper line. #msgisaria-live="polite", so error copy is announced; the input keeps focus.- The SVG is
role="img"with a<title>that says the drawing follows the form. Decorative inline icons arearia-hidden. - The hidden half of
.slotusesvisibility:hidden, so it leaves the tab order and the accessibility tree. - On success, focus moves to the note (
tabindex=-1), which is also polite-live, so the address is read out. - Tab order: input → Join the club → (after success) Use a different address.
- Contrast: ink on paper 12.3:1,
--ink-2on paper 6.6:1, red kicker and error on paper 5.0:1, button label on red 5.7:1, input border ink on paper well above 3:1. - Hit targets: input and button 54px tall.
Responsive rules
- ≥ 1280: as specified, two columns 600px / 1fr.
- 1024: columns shrink proportionally; the SVG scales with its column, the headline holds 64px and wraps to three lines.
- ≤ 900: one column, padding 28 20 40; the drawing comes first, max 440px wide and centred; headline 40px; lede 17px; the form row stacks (input full width with
flex:none, button full width and centred); facts stack with 12px gaps and lose their rules. - 390: the mailbox sits at the top of the frame, the form and button are visible above the fold, nothing scrolls sideways; long addresses wrap anywhere in the note.
Acceptance checklist
Always
- The drawing is inline SVG with ink strokes 2.2px round-capped, hatching at 1.1px, and a displacement filter (turbulence
.04, scale 3.2) on the whole drawing group. - Typing opens the door to
scaleY(.84)from the bottom hinge; clearing closes it. - A valid address raises the flag 90° with a spring; invalid lowers it.
- Empty and malformed submits show different error sentences, set
aria-invalid, keep focus in the input and wobble the flag (replayable). - A valid submit runs a single 2400ms sequence: fold, door open, envelope in, door close; the flag stays up.
- The thank-you note shows the exact address typed and replaces the form without moving the headline.
- The reset control restores the idle drawing and focuses the input.
- Input and button are 54px tall; the input border is 1.5px ink on the input itself (≥ 3:1).
- Reduced motion lands on end states with no tweening.
- At 390px the drawing stays above the form and nothing overflows sideways.
This demo
- Club: The Hedgerow Post; headline “Write to a stranger. Get a real letter back.” with the second sentence italic.
- Facts: Wed 4 Nov / 2,418 members in 31 countries / Free, you buy your own stamp.
- Door reads “No. 12”; the envelope is lilac
#d8cce6with a red stamp; a robin sits on the box.
Implementation notes
One class per form state on the SVG. CSS does the drawing; JS only toggles classes. Use transform-box: view-box so the origins are in viewBox units, and remember that a CSS transform replaces an element’s transform attribute, so place animated groups with CSS, not attributes.
#door, #flag, #env, #env .flap, #env .letter { transform-box: view-box }
#door { transform-origin: 200px 316px; transition: transform 320ms var(--ease-out) }
#flag { transform-origin: 420px 262px; transition: transform 520ms var(--spring) }
.typing #door { transform: scaleY(.84) }
.up #flag { transform: rotate(90deg) }
#env { opacity: 0; transform: translate(96px, 104px) } /* drawn centred on 0,0 */
#env .flap { transform-origin: 0 -39px }
.sent #env { animation: envGo 2400ms var(--ease) both }
.sent #env .flap { animation: flapFold 2400ms var(--ease) both }
.sent #door { animation: doorPost 2400ms var(--ease) both }
@keyframes flapFold { 0%, 34% { transform: scaleY(1) } 50%, 100% { transform: scaleY(-1) } }
@keyframes doorPost { 0%, 38% { transform: scaleY(.84) } 52%, 78% { transform: scaleY(-.46) } 92%, 100% { transform: scaleY(1) } }
Sync the drawing on every keystroke, wobble on a bad submit. Force a reflow between removing and re-adding the class so the wobble replays.
const re = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
const sync = () => { const v = input.value.trim();
svg.classList.toggle('typing', v.length > 0);
svg.classList.toggle('up', re.test(v)); };
const bad = (text) => { msg.className = 'msg bad'; msg.lastChild.textContent = text;
input.setAttribute('aria-invalid', 'true');
svg.classList.remove('wobble'); void svg.getBoundingClientRect(); svg.classList.add('wobble');
input.focus(); };
Stack the form and the note in one grid cell so the swap never shifts the headline, and hide the inactive one with visibility, not display, so the cell keeps the taller height.
.slot { display: grid; align-items: start }
.slot > * { grid-area: 1 / 1 }
.off { visibility: hidden }
Common mistakes: using filter: blur or the rough filter on text (keep it on the drawing only); placing the envelope with an SVG transform attribute and then losing it to the CSS keyframes; letting the flag plate collide with the robin (the robin sits mid-box, the flag at the back); drawing the meadow as a hard rectangle instead of a clipped hand-cut patch; forgetting novalidate, so Chrome’s bubble fights the live region.
Details
- Palette
- Type
- Petrona · Mali
- Motion
- Rich motion
- Build
- One session
- Tags