design-sense · git:20260905.f43dcc2 · 2026-09-05 · sha256 34858e7003efae6d
design-sense git:20260905.f43dcc2A
Immutable. This exact content is served forever at /api/v1/blob/34858e7003efae6d.
--- name: design-sense description: Compose interfaces that hold together — skeleton before aesthetics, one alignment per row, sizing by declaration. Use when building or reviewing any UI component, fixing "it looks off but I cannot say why", or adding an element to an existing layout. --- # Design sense Most UI that "looks off" is not ugly. It is **structurally inconsistent** — two alignments in one row, two things that should match sized by different mechanisms, a number column that shifts with its digit count. The viewer cannot name it and reports a feeling. This skill is how to not create that feeling. Every rule below came from a real defect. None is a preference. --- ## 1. The order of operations Build in this order. Doing them out of order is how a layout ends up needing to be rebuilt rather than adjusted. 1. **Skeleton.** Boxes, widths, alignment. No colour, no type, no borders. If it is wrong here, no amount of styling saves it — and styling makes the wrongness harder to see, because now there is something to look at. 2. **Rhythm.** One spacing scale. Every gap is a step on it. 3. **Hierarchy.** Size and weight decide what is read first. Still no colour. 4. **Theme.** Colour, borders, shadow — applied last, on top of a layout that already works in greyscale. **The test:** turn the colour off. If you cannot tell what matters, the problem is in step 3, and reaching for a brighter accent will not fix it. --- ## 2. Composition rules ### One alignment per row A row that aligns its children to the bottom, containing a label aligned to the centre, containing a note on its own baseline, is three alignments in one line. Nobody can name it; everybody sees it. Pick one — `items-center` for controls, `items-baseline` for text of different sizes — and let every child inherit it. A child that needs to differ says so explicitly (`self-center`), and that exception should be rare enough to notice. **Failure seen:** a bid bar with the row at `items-end`, the label at `items-center`, and the caption on neither. ### A child in a top-aligned row must declare itself Add an element to a row aligned at the top and it will sit flush against the top edge and clip. Nothing is wrong with the element; it never said where it goes. `self-center` is not decoration — it is the element stating its place. ### Size siblings by the same mechanism Two controls that should be the same height must be sized the same way. One sized by padding and one by a minimum height can only ever *coincide* — and they stop coinciding the moment either one's padding changes. Declare it: both `h-12`. Matching by accident is not matching. ### Containers assign meaning Anything inside a bordered, tinted, single-hover-target panel is read as part of that panel's purpose. Put a view count inside a box holding a price and a Buy button and people read it as something they are being charged for. **Before adding an element, ask what its container already claims.** If the answer is wrong, the element belongs outside the container, not restyled within it. ### Width follows content, not availability Two short fields stretched to full width because the row was there, while the thing that belonged beside them sat alone underneath. A field's width should suggest the length of what goes in it: a name is short, a description is long, and a full-width name field silently promises otherwise. ### Labels vary; inputs must not Labels wrap. A two-line label above an input pushes that input a line below its neighbour, and the row breaks. Make each field a flex column with the input pinned to the bottom (`h-full flex-col` + `mt-auto`), so every input in a row sits on one line whatever its label does. Do not solve it by shortening the label — the next long label reintroduces it. ### Numbers in a column are right-aligned and fixed-width `0` and `1,240` starting at different left edges makes a column look broken even when every figure is correct. Right-align, fix the width, use tabular figures. This is the cheapest credibility in an interface that shows numbers. --- ## 3. Spacing **One scale, no exceptions.** 4 / 8 / 12 / 16 / 24 / 32 / 48 / 64. A gap that is not on it is a gap somebody guessed. **Space belongs to relationships, not elements.** Related things sit closer than unrelated things — that proximity is the only grouping signal that works without a border. If you are reaching for a divider, first check whether the spacing already says it. **Padding is symmetric unless asymmetry means something.** `pl-3 pr-2` is almost always a mistake somebody stopped noticing. **A sticky element changes the spacing of what it sits over.** A page ending in generous bottom padding, under a sticky bar, under a footer, produces a dead band nobody designed. When something sticks, the space beneath it is its responsibility, not the page's. --- ## 4. Responsive ### Design the narrow case first Not because mobile matters more, but because the narrow case forces the priority question — what is this row FOR — and the answer improves the wide case too. ### Dropping is a decision, and it is about width, not importance On a 375px row, an avatar and a held-time clock cost ~163px, and the name truncates to "Meridia…" to pay for it. The name is the row. Both get dropped. State the trade in a comment where the drop happens: what was removed, what it bought, why the remaining thing won. Otherwise somebody restores it in six months and the name truncates again. ### Fold economy Count what sits above the primary content on a phone. Six facts and two progress bars above a leaderboard means the thing people came for starts below the fold. Rule: **one headline, one primary action, at most two supporting facts.** Everything else folds behind a tap, or moves below the content. ### A breakpoint above the common laptop is effectively "off" `hidden 2xl:block` (1536px) means most laptops never render it. Something built and invisible is worse than something missing — it reads as built. **Verify at the width people actually use**, not the width you have. --- ## 5. Mobile hard rules - **Inputs at 16px** or iOS zooms on focus and never zooms back. Enforce it unlayered, and see §6. - **Standalone tap targets ≥ 44px.** Links inside a sentence are exempt — boxing prose links breaks the paragraph. - **Nothing scrolls horizontally.** Wide content (tables, tickers, code) scrolls inside its own container, never the page. - **Sticky bottom bars pad with `env(safe-area-inset-bottom)`**, and the viewport must be set to `cover` or that inset is always zero. --- ## 6. The traps **Layered `!important` beats unlayered `!important`.** In Tailwind v4, an `!important` inside `@layer utilities` wins against an unlayered one. So a theme override written as unlayered `!important` silently loses. Use plain unlayered rules: unlayered beats layered regardless of specificity, and needs no `!important` at all. **Framework state loses to background re-renders.** A scroll-driven UI flag in component state is reset by any router refresh or polling revalidation. Put the flag on the document root and key the CSS off it — outside the framework, nothing the framework does can undo it. **Grepping rendered HTML for a label misses it.** React inserts comment nodes between a literal and an interpolated value, so `held 3d` is `held <!-- -->3d`. Match on a class or an attribute instead. **Same number, different meaning by context.** "0 views" on a leaderboard row reads as a verdict on the person; on their own profile it reads as the honest state of a new page. Decide per context whether zero is information or an accusation. --- ## 7. Review checklist Run before calling a component done: - [ ] Does it hold up in greyscale? - [ ] One alignment per row, exceptions declared? - [ ] Do siblings that match, match **by declaration**? - [ ] Is every gap on the scale? - [ ] Do numbers line up down their column? - [ ] Does each element's container claim what the element means? - [ ] Does it survive the longest realistic label, name and number? - [ ] Checked at 375px, 768px, 1280px — and 1440px, not just your own screen? - [ ] Does any tap target fall under 44px? - [ ] Any horizontal scroll at 375px? **And the one that catches most of it:** open the real page at the real width. Verifying that the code renders is not verifying that a person can see it. --- ## Known limits - **It is composition, not visual design.** Nothing here chooses a palette, a typeface, or a brand voice. A layout can pass every rule above and still look generic — structural consistency is the floor, not taste. - **It cannot see the render.** Every rule is checkable by reading code except the one that matters most (open the real page at the real width). The checklist can be walked honestly and still miss what a screenshot would show in a second. - **No accessibility coverage beyond tap targets.** Contrast ratios, focus order, screen-reader semantics, motion sensitivity and keyboard traps are out of scope — use the accessibility review path for those. - **The numbers are conventions, not measurements.** 44px targets and the 375/768/1280/1440 widths are defaults that held up in practice, not thresholds derived from this project's own analytics. A product whose real traffic sits elsewhere should use its real widths. - **It assumes a spacing scale exists.** On a codebase with no scale, "every gap is a step on it" is advice about work that hasn't been done yet. - **Silent on animation, and on data-dense surfaces** — tables, dashboards and charts have their own alignment economics that these rules only partly reach.