design-sense · git:20260826.fea4802 · 2026-08-26 · sha256 3142ba745aa3944a
design-sense git:20260826.fea4802A
Immutable. This exact content is served forever at /api/v1/blob/3142ba745aa3944a.
--- 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.