Good UI composition is about understanding information hierarchy first. When you decide what information leads and what supports it, the layout naturally follows. When you reach for components first, you often end up wrapping content in unnecessary cards, borders, and badges that obscure the actual data.
1. List Chrome vs. Clean Alignment
When presenting a list of items (such as recent activity, documents, or reports), developers frequently wrap each row in a separate bordered container.
- ❌ The Error: Wrapping every row in its own bordered card and placing dates in pill badges. Repeating card boundaries and badges creates heavy visual noise that distracts from the actual content and inflates page height.
- ✅ The Fix: Rely on shared alignment axes, subtle muted text, and vertical whitespace rhythm. Keep pill badges strictly for scan-critical dynamic states (e.g. Failed, Running, Completed), not static metadata like dates.
import { DesignCase } from "@/components/design-case";
/**2. Proximity and Spacing Scales
The Gestalt Law of Proximity states that elements placed close together are instinctively perceived as belonging to the same group. A widespread composition error is using uniform spacing everywhere—between a heading and its text, an icon and its label, a key and its value, or an input and its caption.
- ❌ The Error: Uniform spacing across all visual relationships (e.g. using
gap-4uniformly between headers, descriptions, controls, and subsequent sections). When the distance between tightly bound sub-elements is identical to the distance between independent sections, grouping dissolves, forcing the eye to work to deduce which element belongs to which. - ✅ The Fix: Establish two distinct spacing scales (Internal Tightness vs. External Separation). Keep internal relationships tight (4–6px between an icon and text, a title and its paragraph, or a label and its field), and make the separation between distinct groups 3–4× looser (20–24px). A section heading should always sit noticeably closer to what it introduces than to what came before it.
import { DesignCase } from "@/components/design-case";
import { cn } from "@/lib/utils";
3. Dashboard Metrics Hierarchy
Metric summaries often default to a grid of equal-sized cards, regardless of how important each number actually is.
- ❌ The Error: Presenting metrics as uniform 2x2 grid cards with identical font size and weight. Equal cards imply that all numbers share equal priority, diluting the impact of your primary KPI.
- ✅ The Fix: Use Lead-and-Support composition. Give the hero metric commanding visual weight (
text-3xl font-semibold), while tucking supporting telemetry underneath in clean, compact rows.
import { DesignCase } from "@/components/design-case";
const LEAD = { label: "Requests", value: "12,480" };4. Nested Surfaces (Card Inception)
When grouping complex settings or permissions, it is easy to accumulate nested bordered boxes.
- ❌ The Error: Nesting cards inside cards inside cards. Every extra border adds visual weight without adding meaningful structure, creating a claustrophobic box-within-a-box layout.
- ✅ The Fix: Keep related content on a single surface, separating sub-sections with clean whitespace or a subtle 1px divider (
border-t). Only introduce a separate nested surface when an element can be independently dragged, selected, or dismissed.
import { DesignCase } from "@/components/design-case";
const Nested = () => (5. Divider Extent: Inset It to the Content
A divider is the same 1px border-border the card around it was drawn with, so its length is the part that carries meaning: a line that spans the row is claiming the container's edge, and a line that stops where the text stops stays a mark inside one block.
-
❌ The Error: Letting the line run the row's full width. The card is already one block — a plan, its seats, its renewal date — and the line severs it into three stacked slabs. At that length the divider matches the card's own edge in colour, weight and extent, so the eye reads three boundaries where there is one card and the outer border loses the authority it was drawn to carry. Both ends also run into the corner arc, where the line and the curvature meet with nothing between them.
-
✅ The Fix: Give the divider the same left and right padding the content has, so it shares the text's axis. The ends keep their breathing room, the block stays whole, and the line reads as a pause between two facts rather than a border between two surfaces. Mechanically it comes down to which level holds the
px-4: on the card, everydivide-yline insets with the content for free; on the rows, the line runs to the card's edge and has to be pulled back with anmx-4hairline or a pseudo-element.The two cards are one
px-4moved a level, and they read as different objects rather than different measurements — 32px off each end of a 1400px card is 4% of the line, and all of what it says.
import { DesignCase } from "@/components/design-case";
const ROWS = [⚠️ Boundary: The line follows the content column, so a surface with no surrounding card — a list at the page level, a table whose header rule is the table's own top edge — has nothing to inset from, and there the line is a boundary rather than a pause.
The Rule of Thumb: The Strip-Down Test
If you strip away all decorative borders, background fills, and badge pills from your layout, does the visual hierarchy and logical flow of the page remain crystal clear?
- If yes, your composition is structurally sound.
- If no, decorative chrome is masking weak information hierarchy.