Design Lounge
Install

Nº 534 of 543

Signup form that rises to the caret

A neo-brutalist signup card: inputs lift up-left into a blue shadow on focus, the button sinks on hover, an ink-bordered error box, and a rotated pink stamp for the done state.

Preview

Open full size
1280 × 800 · 9.1 KB · 114 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 "Signup form that rises to the caret" (piece form-brutal-signup) 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

174 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. When a kit is locked, map the colours onto the kit tokens and keep the 2px ink borders and the hard 0-blur shadows.

What it is

The sample-issue signup for Stapler, a fictional zine printer. Left: a yellow kicker tag, a 60px uppercase headline, a lede and three claims, each led by a 44px bordered tile holding one big figure. Right: a 520px card with an 8px shadow holding a name field, an email field, a select, a drawn checkbox and one yellow button. The form has opposite motions for the two things you touch: an input rises 4px up-left and gains a blue shadow when it has focus, as if lifting toward the caret; the button sinks into its shadow on hover. A bad email turns the field’s border and shadow red and shows an ink-bordered error box; a good submit swaps the form for a pink “YOU’RE IN” stamp that lands at -6°, scaling from 1.6 to 1 in 240ms, with the email written back and a “Send another” button. The detail worth copying is the rise/sink pair, which gives a flat page a clear sense of what is a field and what is an action without a third colour.

Reference behaviour

  1. First frame: kicker “Issue zero”, headline “Get the sample issue”, lede, three claims (16 pages, 4 days, 0 cost). Card: “Where should it go?”, fields Your name, Email (required), What do you write? (select, “A newsletter for a company” selected), a checked checkbox “Also post the paper sample…”, button “Send me issue zero”, fine print.
  2. Focus an input or the select: it translates (−4px, −4px) and gains a 4px 4px 0 blue shadow in 120ms. Blur: back.
  3. Submit with an invalid email: the Email field gets a red border and red shadow, aria-invalid="true", and the error box “Write an email with an @ in it…” appears under it (role="alert"). Focus moves to the field. Typing an @ clears the error.
  4. Submit with a valid email: the form becomes invisible, the done layer shows: the stamp “You’re in” animates in, the sentence names the typed email in bold with a 2px underline and says whether the paper copy follows (from the checkbox), and “Send another” takes focus.
  5. “Send another”: the form resets and returns; focus goes to the name field.
  6. The checkbox is a drawn 22px square: checked = yellow fill with a 10px ink square inside.
  7. Hover the button: it moves (4px, 4px) and its shadow collapses in 120ms. Active: the same with a --surface-2 fill.

Structure

1280 × 800, bg #F6F1E4, wrap max 1120, padding 64/40, grid minmax(0,1fr) | 520px, gap 72
┌ copy ───────────────────────────┐  ┌ card 520, 8px shadow, padding 28 ───────────┐
│ [ISSUE ZERO] yellow tag         │  │ WHERE SHOULD IT GO?  26/1                    │
│ GET THE SAMPLE ISSUE  60/.95    │  │ YOUR NAME                                    │
│ lede 18px 40ch                  │  │ ┌──────────────────────────────────────┐ 48  │
│ [16] Pages, folded from…        │  │ EMAIL                                        │
│ [4d] From the form to…  (pink)  │  │ ┌──────────────────────────────────────┐     │
│ [0 ] Cost. The sample…  (blue)  │  │ (error box, red, when bad)                   │
│                                 │  │ WHAT DO YOU WRITE?                           │
│                                 │  │ ┌──────────────────────────────── ⌄ ┐       │
│                                 │  │ [■] Also post the paper sample…              │
│                                 │  │ [ Send me issue zero →        ] 52 yellow    │
│                                 │  │ fine print 13px                              │
└─────────────────────────────────┘  └──────────────────────────────────────────────┘
done layer (same box): [YOU'RE IN] pink stamp -6° · sentence with the email · [Send another]
  • main.wrap → copy div (span.kicker, h1, p.lede, ul.claims with b tiles) and div.card#card.
  • form#form[novalidate] → h2, three .fld (label + input/select; the select’s wrapper draws the chevron with ::after), label.chk with the checkbox, button.btn[type=submit], p.fine.
  • div.done[aria-live=polite] → span.stamp, p#done-msg, button.btn.ghost#again. .card.is-done hides the form (visibility: hidden) and shows the layer.

Tokens

:root {
  --bg: #f6f1e4;  --surface: #fffaf0;  --surface-2: #ece4d0;
  --ink: #17151a;  --ink-2: #4a4650;  --ink-3: #76717d;
  --yellow: #ffd23f;  --blue: #2f5bff;  --blue-ink: #fffdf8;  --pink: #ff5c9a;
  --danger: #d8322a;  --danger-soft: #fbe3e0;

  --display: "Archivo Black", Impact, sans-serif;
  --sans: "Archivo", system-ui, sans-serif;

  --bw: 2px;  --off: 4px;  --off-lg: 8px;
  --r: 4px;  --r-card: 6px;                 /* family radii; the tag, checkbox and stamp are 0 */
  --field-h: 48px;  --btn-h: 52px;  --chk: 22px;

  --t-micro: 120ms;  --t-swap: 240ms;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --expo: cubic-bezier(.16, 1, .3, 1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
HeadlineArchivo Black60px4000.95−0.02emUPPERCASE
StampArchivo Black54px4001−0.02emUPPERCASE, --pink, 4px pink border
Card titleArchivo Black26px4001−0.01emUPPERCASE
Claim figureArchivo Black18px40010in a 44px tile
LedeArchivo18px5001.50sentence, --ink-2
Done sentenceArchivo17px5001.50sentence; the email 700 with a 2px underline
ButtonArchivo17px70010sentence
Field labelArchivo12px7001+0.08emUPPERCASE
KickerArchivo12px7001+0.12emUPPERCASE in a yellow tag
Input textArchivo16px40010as typed; placeholder --ink-3
Error, fine printArchivo13px700 / 5001.50sentence

Motion

ElementTriggerPropertyFrom → ToDurationEasing
input, selectfocus-visibletransform, box-shadownone, none → translate(−4px,−4px), 4px 4px 0 var(--blue)120ms--ease
.btnhovertransform, box-shadownone, 4px 4px 0 → translate(4px,4px), 0 0 0120ms--ease
.btnactivebackgroundfill → --surface-2120ms--ease
checkboxchangebackground--surface → --yellow120ms--ease
.stampdonetransform, opacityrotate(−6deg) scale(1.6), 0 → rotate(−6deg) scale(1), 1240ms--expo

Reduced motion: transitions and the stamp animation 1ms. The states are identical.

States

  • Input default: cream, ink border, no shadow. Focus: raised with a blue shadow. Invalid: red border and red 4px shadow, error box shown, aria-invalid="true".
  • Select: same as input, with a drawn chevron at the right (a 9px square rotated 45°, two borders).
  • Checkbox off / on: empty square / yellow square with a 10px ink centre.
  • Button default / hover / active / focus-visible: yellow, pushed, pushed + --surface-2, 2px blue outline at 3px offset.
  • Card done: form invisible, stamp + sentence + ghost button visible. The card keeps its size, so nothing jumps.

Accessibility

  • Every field has a visible <label for>. The email field is required, type="email", aria-describedby points at the error, which is role="alert" so it is read when it appears.
  • The form uses novalidate and does its own check, so the error style and copy are the same in every browser; keep the native check if you prefer, but then style :user-invalid.
  • The done layer is aria-live="polite"; focus moves to “Send another” after a submit, and back to the name field after a reset.
  • The checkbox is a real <input type="checkbox"> with appearance: none; Space toggles it, and the label is the hit area.
  • Contrast: ink on cream 15.2:1; --danger on --danger-soft 5.4:1; --pink stamp on cream is decoration over a real sentence (not relied on alone); --ink-3 placeholder on --surface 5.2:1.
  • Hit targets: fields 48px, button 52px, checkbox 22px with the full label row as target, ghost button 44px.

Responsive rules

  • ≥ 1280: two columns, card 520px, headline 60px.
  • 1024–1279: card 460px, gap 48, headline 52px.
  • 768–1023: one column, the card below the copy, max-width 560px.
  • < 640: padding 20px, headline 40px, claims’ tiles 40px, card padding 20px, the stamp 40px. Shadow offsets stay 4px / 8px.

Acceptance checklist

Always

  • Every border is 2px --ink (red on an invalid field); every shadow is hard with no blur; the tag, checkbox and stamp have 0 radius, controls and the card use the family radii.
  • An input or select with focus rises (−4px, −4px) and shows a blue 4px shadow; the submit button sinks (4px, 4px) on hover.
  • An invalid email sets aria-invalid, shows an alert error box under the field and moves focus there; typing an @ clears it.
  • A valid submit shows the done layer with the typed email, keeps the card’s size, and moves focus to the reset button; the reset returns the form.
  • The checkbox is drawn (22px square, yellow when checked) and is a real input.
  • One primary button, full width. The reset button is cream (ghost), not a second yellow.
  • Focus rings are visible on every control, outside the shadows.
  • Reduced motion: 1ms transitions, no stamp zoom.

This demo

  • Copy: “Get the sample issue”, claims 16 / 4d / 0, card title “Where should it go?”, button “Send me issue zero”, stamp “You’re in”.
  • The done sentence names the email and adds “The paper copy follows in about four days.” only when the checkbox was on.

Implementation notes

Rise and sink. The same offset, opposite sign, a different shadow colour:

.fld input:focus-visible, .fld select:focus-visible {
  outline: none;
  transform: translate(calc(var(--off) * -1), calc(var(--off) * -1));
  box-shadow: var(--off) var(--off) 0 var(--blue);
}
.btn:hover, .btn:active { transform: translate(var(--off), var(--off)); box-shadow: 0 0 0 var(--ink); }

Validation without a library. One regex, one class, one attribute:

form.addEventListener('submit', (e) => {
  e.preventDefault();
  const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim());
  field.classList.toggle('bad', !ok);
  email.setAttribute('aria-invalid', String(!ok));
  if (!ok) { email.focus(); return; }
  card.classList.add('is-done');
  again.focus();
});

The stamp lands rather than fades:

.stamp { transform-origin: left bottom; animation: stamp 240ms cubic-bezier(.16,1,.3,1) both; }
@keyframes stamp { from { transform: rotate(-6deg) scale(1.6); opacity: 0 } to { transform: rotate(-6deg) scale(1); opacity: 1 } }

Common mistakes: a blurred focus glow instead of the hard blue shadow; a native select arrow next to the drawn chevron (set appearance: none); an error colour on the text only, with no box; swapping the card for a shorter one so the page jumps; forgetting to escape the typed email before writing it into the done sentence.

Details

Palette
Type
Archivo Black · Archivo
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Abel

A catalogue. One product at a time.

All sources