{"slug": "form-spacing-skill-for-ai-coding-agents-an-8-20-24-48px-spacing-system-for-ui-on", "title": "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\".", "summary": "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.", "body_md": "| name | form-spacing | \n|---|---|\n| 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\". | \n\nDon'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.\n\nSource: Zander Whitehurst (@zander_supafast, Memorisely), \"Stop guessing your UI form spacing\".\n\n| Relationship | Gap | Tailwind | Why | \n|---|---|---|---|\n| Container padding, top/bottom | **48px** | `py-12` | The form needs room to breathe. Never let it touch the card or screen edge. | \n| Container padding, left/right | **40px** | `px-10` | Same reason | \n| Title → caption/subtitle | **8px** | `gap-2` | They are one heading unit | \n| Between content groups | **24px** | `gap-6` | Separates header / fields / actions / alt-actions | \n| Label → its input | **8px** | `gap-2` /`mb-2` | The label belongs to the input below it | \n| Field → next field | **20px** | `gap-5` | Clearly more than 8px, so no label floats toward the previous input. Leaves room for a helper. | \n| Input → helper or error text | **4px** | `mt-1` | The helper is part of the input it describes | \n| Last field → primary button | **24px** | `gap-6` | Close enough to belong, without being squished against the input | \n| Divider (\"Or continue with\") | **24px above and below** | `gap-6` | A divider is a group boundary | \n| Button → button (stacked or side by side) | **8px** | `gap-2` | Related alternatives are one cluster | \n\nEvery 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.\n\n**Dark mode uses identical spacing.** Only color tokens change between themes. Never put spacing inside theme overrides.\n\n**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.\n\nDivide the form into groups first, then apply the table:\n\n```\n┌─ card: 48px top/bottom, 40px left/right ─┐\n│  [Header]   title ─8─ caption            │\n│      24                                  │\n│  [Fields]   label ─8─ input ─4─ helper   │\n│             ──20── next field            │\n│      24                                  │\n│  [Primary]  submit button                │\n│      24                                  │\n│  [Divider]  ── Or continue with ──       │\n│      24                                  │\n│  [Alt]      button ─8─ button            │\n└──────────────────────────────────────────┘\n```\n\nSet 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.\n\n```\n<style>\n  :root {\n    --form-pad-y: 48px; --form-pad-x: 40px;\n    --space-group: 24px;  /* content groups, last field → button, around divider */\n    --space-field: 20px;  /* field → field */\n    --space-tight: 8px;   /* title → caption, label → input, button → button */\n    --space-helper: 4px;  /* input → helper/error */\n  }\n  .card   { padding: var(--form-pad-y) var(--form-pad-x); }\n  .form   { display: flex; flex-direction: column; gap: var(--space-group); }\n  .header, .alt-actions { display: flex; flex-direction: column; gap: var(--space-tight); }\n  .header > * { margin: 0; } /* kill default h1/p margins or they add to the gap (Tailwind's preflight already does) */\n  .fields { display: flex; flex-direction: column; gap: var(--space-field); }\n  .field  { display: flex; flex-direction: column; }\n  .field label { margin-bottom: var(--space-tight); }\n  .field .help { margin: var(--space-helper) 0 0; }\n</style>\n\n<div class=\"card\">\n  <form class=\"form\">\n    <div class=\"header\">\n      <h1>Create your account</h1>\n      <p>Get started with your free account today</p>\n    </div>\n    <div class=\"fields\">\n      <div class=\"field\">\n        <label for=\"name\">Your name</label>\n        <input id=\"name\" />\n      </div>\n      <div class=\"field\">\n        <label for=\"email\">Work email</label>\n        <input id=\"email\" type=\"email\" aria-describedby=\"email-help\" />\n        <p class=\"help\" id=\"email-help\">Use your work email to collaborate with colleagues</p>\n      </div>\n    </div>\n    <button type=\"submit\">Continue</button>\n    <div class=\"divider\">Or continue with</div>\n    <div class=\"alt-actions\">\n      <button type=\"button\">Continue with Google</button>\n      <button type=\"button\">Continue with Microsoft</button>\n    </div>\n  </form>\n</div>\n```\n\nIn 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`.\n\nThe 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.\n\nThese are extensions consistent with the system. Don't invent new gap values for them.\n\n- **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.\n- **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.\n- **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.\n- **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.\n- **Validation errors** take the helper slot at 4px under the input. They don't change any other gap.\n- **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.\n- **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.\n- **Card width:** if 40px padding squeezes the inputs, widen the card (for example`max-w-sm` →`max-w-md` ) rather than cutting the padding.\n\nList 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.\n\nIf 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.\n\n| Mistake | Fix | \n|---|---|\n| Form cramped against the card or screen edge ( `p-4` ) | 48px vertical, 40px horizontal | \n| One uniform gap everywhere | Use the hierarchy: 4 < 8 < 20 < 24 | \n| Label as close to the previous input as to its own | Label → input 8px, field → field 20px | \n| Helper floating halfway to the next field | Helper 4px under its input | \n| Submit button jammed against the last input | 24px | \n| Stacked alternative buttons spread far apart | 8px | \n| Default `h1` /`p` margins quietly stacking with`gap` | Reset margins inside gap containers | \n| Different spacing in `dark:` variants | Spacing is theme-independent |", "url": "https://wpnews.pro/news/form-spacing-skill-for-ai-coding-agents-an-8-20-24-48px-spacing-system-for-ui-on", "canonical_source": "https://gist.github.com/patrykcieszkowski/85a9279b254d2973487f190ec635d7a3", "published_at": "2026-10-29 18:32:04+00:00", "updated_at": "2026-10-29 22:18:27.255519+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "generative-ai"], "entities": ["Zander Whitehurst", "Memorisely", "Tailwind", "React", "SwiftUI", "Flutter"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/form-spacing-skill-for-ai-coding-agents-an-8-20-24-48px-spacing-system-for-ui-on", "markdown": "https://wpnews.pro/news/form-spacing-skill-for-ai-coding-agents-an-8-20-24-48px-spacing-system-for-ui-on.md", "text": "https://wpnews.pro/news/form-spacing-skill-for-ai-coding-agents-an-8-20-24-48px-spacing-system-for-ui-on.txt", "jsonld": "https://wpnews.pro/news/form-spacing-skill-for-ai-coding-agents-an-8-20-24-48px-spacing-system-for-ui-on.jsonld"}}