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.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add hamza-ali-shahjahan/hamzaish --skill design-sense --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Design Sense?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/hamza-ali-shahjahan-design-sense)More formats (shields.io, HTML) on the badges page.
---
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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!