cd /news/ai-agents/form-spacing-skill-for-ai-coding-age… Β· home β€Ί topics β€Ί ai-agents β€Ί article
[ARTICLE Β· art-160592] src=gist.github.com β†— pub= topic=ai-agents verified=true sentiment=↑ positive

Form-spacing skill for AI coding agents: an 8 / 20 / 24 / 48px spacing system for UI forms, based on Zander Whitehurst's "Stop guessing your UI form spacing".

A developer published a form-spacing skill for AI coding agents that encodes a fixed 4px-grid spacing system for UI forms, drawn from Zander Whitehurst's "Stop guessing your UI form spacing." The skill assigns specific gaps by relationship β€” 8px for label-to-input and title-to-caption, 20px between fields, 24px between content groups and around dividers, and 48px/40px container padding β€” and instructs agents to set gaps once on the parent container rather than scattering margins. It covers HTML/CSS, Tailwind, React, Vue, Svelte, SwiftUI, Flutter and Figma specs, with identical spacing in dark mode.

by read7 min views12 publishedOct 29, 2026
name form-spacing
description Use when building, styling, or reviewing any UI form or auth card β€” sign-up, login, checkout, settings, contact, onboarding, waitlist β€” in HTML/CSS, Tailwind, React, Vue, Svelte, SwiftUI, Flutter, or a Figma/design spec. Also use when a form looks cramped, unbalanced, or "off", or its spacing is eyeballed or inconsistent, even if the user never says the word "spacing".

Don't eyeball form spacing. Use a fixed set of gaps, where proximity carries meaning: small gaps tie things together (a label to its input, a helper to its input), and larger gaps separate one group from the next. When every gap comes from the table below, the form reads correctly at a glance and looks the same in every theme.

Source: Zander Whitehurst (@zander_supafast, Memorisely), "Stop guessing your UI form spacing".

Relationship Gap Tailwind Why
Container padding, top/bottom 48px py-12 The form needs room to breathe. Never let it touch the card or screen edge.
Container padding, left/right 40px px-10 Same reason
Title β†’ caption/subtitle 8px gap-2 They are one heading unit
Between content groups 24px gap-6 Separates header / fields / actions / alt-actions
Label β†’ its input 8px gap-2 /mb-2 The label belongs to the input below it
Field β†’ next field 20px gap-5 Clearly more than 8px, so no label floats toward the previous input. Leaves room for a helper.
Input β†’ helper or error text 4px mt-1 The helper is part of the input it describes
Last field β†’ primary button 24px gap-6 Close enough to belong, without being squished against the input
Divider ("Or continue with") 24px above and below gap-6 A divider is a group boundary
Button β†’ button (stacked or side by side) 8px gap-2 Related alternatives are one cluster

Every value is on a 4px grid. In rem (16px base): 0.25 / 0.5 / 1.25 / 1.5 / 2.5 / 3. Use the same numbers as pt/dp in SwiftUI and Flutter. Gaps are measured between element boxes, the way CSS gap works, not between visible glyphs. Container padding sits inside any card border.

Dark mode uses identical spacing. Only color tokens change between themes. Never put spacing inside theme overrides.

Scope: the table governs the space between form elements and the container padding. The insides of components are not covered. That includes input/button inner padding and height, the icon β†’ text gap inside a button, and the line β†’ text gap inside a divider. Keep the project's existing component styles for those.

Divide the form into groups first, then apply the table:

β”Œβ”€ card: 48px top/bottom, 40px left/right ─┐
β”‚  [Header]   title ─8─ caption            β”‚
β”‚      24                                  β”‚
β”‚  [Fields]   label ─8─ input ─4─ helper   β”‚
β”‚             ──20── next field            β”‚
β”‚      24                                  β”‚
β”‚  [Primary]  submit button                β”‚
β”‚      24                                  β”‚
β”‚  [Divider]  ── Or continue with ──       β”‚
β”‚      24                                  β”‚
β”‚  [Alt]      button ─8─ button            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Set each gap once, on the parent container (flex/ grid + gap), rather than scattering mt-*/ mb-* on individual elements. Stray margins are how forms drift out of the system. Inside a field, the label and helper are the only places where margins make sense.

<style>
  :root {
    --form-pad-y: 48px; --form-pad-x: 40px;
    --space-group: 24px;  /* content groups, last field β†’ button, around divider */
    --space-field: 20px;  /* field β†’ field */
    --space-tight: 8px;   /* title β†’ caption, label β†’ input, button β†’ button */
    --space-helper: 4px;  /* input β†’ helper/error */
  }
  .card   { padding: var(--form-pad-y) var(--form-pad-x); }
  .form   { display: flex; flex-direction: column; gap: var(--space-group); }
  .header, .alt-actions { display: flex; flex-direction: column; gap: var(--space-tight); }
  .header > * { margin: 0; } /* kill default h1/p margins or they add to the gap (Tailwind's preflight already does) */
  .fields { display: flex; flex-direction: column; gap: var(--space-field); }
  .field  { display: flex; flex-direction: column; }
  .field label { margin-bottom: var(--space-tight); }
  .field .help { margin: var(--space-helper) 0 0; }
</style>

<div class="card">
  <form class="form">
    <div class="header">
      <h1>Create your account</h1>
      <p>Get started with your free account today</p>
    </div>
    <div class="fields">
      <div class="field">
        <label for="name">Your name</label>
        <input id="name" />
      </div>
      <div class="field">
        <label for="email">Work email</label>
        <input id="email" type="email" aria-describedby="email-help" />
        <p class="help" id="email-help">Use your work email to collaborate with colleagues</p>
      </div>
    </div>
    <button type="submit">Continue</button>
    <div class="divider">Or continue with</div>
    <div class="alt-actions">
      <button type="button">Continue with Google</button>
      <button type="button">Continue with Microsoft</button>
    </div>
  </form>
</div>

In Tailwind, the same structure is: card px-10 py-12, form flex flex-col gap-6, header flex flex-col gap-2, fields flex flex-col gap-5, field flex flex-col, label mb-2, helper mt-1, and alt actions flex flex-col gap-2.

The header doesn't have to be inside <form>. If it sits outside, wrap the header and the form in a flex flex-col gap-6 div. Nested gap containers don't add up, because gap only applies between siblings.

These are extensions consistent with the system. Don't invent new gap values for them.

  • Side-by-side fields (city | postcode): use 20px between columns, the same as field β†’ field. Top-align them (items-start ). The next field sits 20px below the taller column.
  • Several sections (shipping, then payment): each section is a content group with its own header (title 8 caption, then 24 to its fields). Keep 24px between sections, and separate them with a divider that has 24px on each side. A page title above all the sections is just another header group, 24px above the first section.
  • Checkbox and radio rows: the control β†’ its label is 8px across, and a description under the label takes the helper slot (4px). If the rows have descriptions, they are fields, 20px apart. If they are bare options in one list, they are a cluster, 8px apart like stacked buttons.
  • Secondary action under the primary button ("Back to cart", "Cancel"): it joins the primary's action group at 8px button β†’ button. That whole group sits 24px below the last field.
  • Validation errors take the helper slot at 4px under the input. They don't change any other gap.
  • Narrow screens (under 640px): the internal gaps stay exactly the same. Horizontal container padding drops to 24px (px-6 sm:px-10 ) and vertical stays at 48px.
  • Space around the card: keep the card off the screen edge with a page gutter of at least 16px on every side (p-4 ). On phones you can instead let the card fill the screen, with its container padding as the gutter.
  • Card width: if 40px padding squeezes the inputs, widen the card (for examplemax-w-sm β†’max-w-md ) rather than cutting the padding.

List every relationship from the table and measure what the code actually produces. Remember that margin s, gap, and default h1/ p margins add up. Report each one as current β†’ target, then fix them by restructuring into groups with gap, not by tweaking individual margins.

If you can render the page (DevTools, Playwright, or a browser tool), evaluate scripts/measure-gaps.js in it. It prints the container padding and every vertical gap in order, so you can check them against the table instead of estimating. Run it in dark mode too, and the numbers should be identical.

Mistake Fix
Form cramped against the card or screen edge ( p-4 ) 48px vertical, 40px horizontal
One uniform gap everywhere Use the hierarchy: 4 < 8 < 20 < 24
Label as close to the previous input as to its own Label β†’ input 8px, field β†’ field 20px
Helper floating halfway to the next field Helper 4px under its input
Submit button jammed against the last input 24px
Stacked alternative buttons spread far apart 8px
Default h1 /p margins quietly stacking withgap Reset margins inside gap containers
Different spacing in dark: variants Spacing is theme-independent
── more in #ai-agents 4 stories Β· sorted by recency
── more on @zander whitehurst 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/form-spacing-skill-f…] indexed:0 read:7min 2026-10-29 Β· β€”