Visual Design Foundations
Visual design in an interface is not decoration. It is the mechanism by which a reader knows what matters, what belongs together, and where to look next, before reading a single word. A screen with correct content and no hierarchy is genuinely harder to use than a plain one with hierarchy, because the reader has to construct the structure themselves every time they look at it.
Five variables do nearly all of the work:
| Variable | Controls |
|---|---|
| Size | Importance. The strongest and bluntest signal available |
| Weight | Emphasis without changing the space a thing occupies |
| Colour | Category and state, never meaning on its own |
| Space | Grouping. The most underused variable in most interfaces |
| Position | Reading order and relative priority |
The sections below take these in the order you would actually apply them: hierarchy first, then the grid that positions things, then the spacing scale, then type, then colour.
1. Visual hierarchy
Before anything else, decide the order in which a first-time viewer should notice things. There should be one clear first place for the eye to land per screen, and a deliberate second and third.
Both panels contain the same six pieces of information. The right one has decided which matters.
The technique is contrast, and contrast requires difference. Two headings at 17 px and 19 px do not read as two levels, they read as an inconsistency. If two things have different importance, make the difference obvious; if they have the same importance, make them identical.
Squint at it. Blur the screen, or look at it from across the room. Whatever still stands out is your real hierarchy, and it is frequently not the one you intended. This is the cheapest visual design test available.
One primary action per view. Three buttons of equal weight is the same as no primary action. Primary gets the filled treatment, secondary gets an outline or a plain text button, tertiary goes in a menu.
2. Layout and grids
A grid exists so that alignment decisions are made once instead of per element. The 12-column grid is the common default because 12 divides by 2, 3, 4 and 6, giving halves, thirds, quarters and sixths without fractional widths.
In CSS you rarely need a column framework any more. Grid and flexbox express the intent directly:
.page {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 1rem;
max-width: 72rem; /* a measure cap, not a device width */
margin-inline: auto;
padding-inline: 1rem; /* the gutter that keeps text off the edge */
}
.main { grid-column: span 8; }
.aside { grid-column: span 4; }
/* Auto-fitting cards need no breakpoints at all. */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1rem;
}Note minmax(0, 1fr) rather than 1fr: without it, a long unbreakable string can force a column wider than its share and blow out the layout.
Alignment is the cheapest quality signal there is. Fewer alignment edges reads as more deliberate. Pick a left edge and hold it down the whole page; the eye notices a 3 px inconsistency without being able to name it.
Cap the content width, not the viewport. A text column should stop at a readable measure regardless of how wide the window is. See section 4.
3. Spacing
Spacing is how grouping is communicated, and it is the variable most often left to chance. The mechanism is the Gestalt principle of proximity: things near each other are read as related, whatever the borders say.
The rule: space within a group must be smaller than space between groups. A label 6 px from its field and 24 px from the next pair needs no lines, boxes or shading to be understood. If you find yourself adding borders to clarify grouping, the spacing is usually wrong.
Use a scale, not arbitrary numbers. A 4 px base with doubling steps is the common choice, since it lands on whole pixels at every zoom level and gives roughly the right granularity:
:root {
--space-1: 0.25rem; /* 4px icon to label */
--space-2: 0.5rem; /* 8px inside a control */
--space-3: 0.75rem; /* 12px label to field */
--space-4: 1rem; /* 16px between fields */
--space-6: 1.5rem; /* 24px between field groups */
--space-8: 2rem; /* 32px between card sections */
--space-12: 3rem; /* 48px between page sections */
--space-16: 4rem; /* 64px major page divisions */
}Two habits that follow from a scale:
- Only ever use values from it. The moment a 13 px gap appears, the system stops guaranteeing rhythm, and the next person copies the 13.
- Apply space in one direction. Margin-bottom on stacked elements, or better,
gapon the container. Mixing top and bottom margins makes collapsing margins your problem and every gap becomes a subtraction puzzle.
White space is not wasted space. The most common note on a first draft interface is that everything needs more room. Density is occasionally correct, for a trading screen or a log viewer, but it should be a decision you can defend rather than the result of filling the viewport.
4. Typography
Most interface text is set by four decisions: the family, the scale, the measure, and the line height.
A type scale gives you distinguishable sizes. Pick a base of 16 px, because that is the browser default and shrinking it fights the user’s own settings, then generate steps by a ratio. 1.25 (major third) is a safe interface ratio; 1.2 is tighter for dense applications; 1.333 and above suits marketing pages.
:root {
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.25rem; /* 20px */
--text-xl: 1.5625rem; /* 25px */
--text-2xl: 1.9375rem; /* 31px */
}Measure: 45 to 75 characters per line, with 66 as a good target. Beyond about 80 the eye loses the line when it returns to the left edge; below about 40 the rhythm breaks. Set it in ch units so it tracks the font rather than guessing pixel widths:
.prose { max-width: 66ch; }Line height scales inversely with size. Body text wants 1.5; a large heading wants 1.1 to 1.25, because at that size generous leading pulls the words apart. Set line height unitless so it inherits proportionally.
Weight and case.
- Two or three weights are enough: regular for body, semibold or bold for headings, plus one more if you need it. Loading six weights costs real download time for a distinction nobody notices.
- Avoid all caps for anything longer than a couple of words. Uppercase removes the word shapes that fast reading depends on, which measurably slows reading.
- Light weights below 300 at small sizes fail contrast in practice even when the colour maths passes, because thin strokes carry less ink.
Alignment. Left-aligned (or start-aligned, which handles right-to-left correctly) for body text. Centre only short runs such as a heading or an empty-state message. Justified text creates rivers of white space in a browser, which lacks proper hyphenation control, so avoid it.
Numbers in tables need tabular figures, or the columns will not line up as the digits change:
.metric { font-variant-numeric: tabular-nums; }Use a system font stack unless a custom face earns its cost. It renders instantly, matches the platform, and covers every script the user has installed.
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}5. Colour
The mistake with colour is treating it as a palette of pretty values. Treat it as a set of roles instead, each with a defined job, and pick values that satisfy those jobs.
| Role | Job |
|---|---|
| Surface / background | The ground everything sits on, usually two or three levels |
| Text | Primary, secondary, disabled: a small ramp, not one colour |
| Border | Separation where space alone is not enough |
| Accent / primary | One colour, for the primary action and active state |
| Semantic | Success, warning, danger, info. Fixed meanings, used nowhere else |
Neutrals do most of the work. A good interface is largely greys, with colour reserved for meaning. If everything is coloured, colour signals nothing. The 60-30-10 guideline is a reasonable starting ratio: 60 percent dominant neutral, 30 percent secondary, 10 percent accent.
Build a ramp, not a swatch. One hue at nine or ten lightness steps gives you hover states, borders, and tints without inventing new colours:
Use a perceptual colour space for the maths. HSL lies about lightness: hsl(60 100% 50%) (yellow) and hsl(240 100% 50%) (blue) claim the same 50 percent and are wildly different to the eye. OKLCH fixes this, so a ramp built in OKLCH has steps that actually look evenly spaced, and two hues at the same lightness really match.
:root {
/* lightness, chroma, hue: evenly stepped and predictable */
--accent-400: oklch(70% 0.14 250);
--accent-500: oklch(62% 0.16 250);
--accent-600: oklch(54% 0.17 250);
}Colour must never be the only carrier of meaning. Roughly 1 in 12 men has a colour vision deficiency, and red-green is the common one, which is exactly the pair most status indicators use. Add an icon, a shape, a label or a pattern. This is a WCAG requirement and it is also just better design, since it survives a greyscale printout and a bright afternoon on a phone. Details in Accessibility.
Contrast minimums (WCAG 2.2 AA): 4.5:1 for normal text, 3:1 for text at 24 px or 19 px bold and above, 3:1 for the visible boundary of interactive components and meaningful graphics. Check with a contrast tool rather than by eye, since eye judgement of contrast is unreliable.
Design light and dark together, not dark as an afterthought. Dark mode is not inverted light mode. Saturated colours vibrate on dark grounds and need to be desaturated; pure black backgrounds with pure white text produce halation, so use a very dark grey with a slightly dimmed white; and shadows stop working as elevation cues, so depth has to come from surface lightness instead. Tokens make this a table of values rather than a second stylesheet, which is the subject of the next notebook.
6. Gestalt principles
The rules by which the visual system groups things before conscious reading. Five of them matter daily.
| Principle | Statement | Interface use |
|---|---|---|
| Proximity | Near things are related | The primary grouping tool. See section 3 |
| Similarity | Alike things are related | Same styling implies same kind. So do not style two different kinds identically |
| Common region | A shared enclosure groups | Cards and panels. Weaker than proximity, so do not rely on a border to fix bad spacing |
| Continuity | The eye follows a line | Alignment along an edge creates an implied path down the page |
| Closure | The eye completes a shape | Why a partly visible row communicates “scroll for more” |
Two corollaries worth stating outright:
- Similarity cuts both ways. If two elements look the same, users will expect them to behave the same. A clickable card next to a non-clickable card in the same style is a design bug, not a user error.
- Proximity beats a border. When spacing and enclosure disagree, spacing wins. That is why a form with even spacing inside a box is still ambiguous.
7. Elevation, depth and borders
Depth answers “what is on top of what”, which matters for menus, dialogs, sticky headers and drag operations.
Three tools, in order of subtlety: background lightness, border, shadow. Use the least that works.
:root {
--shadow-1: 0 1px 2px rgb(0 0 0 / .06); /* resting card */
--shadow-2: 0 2px 8px rgb(0 0 0 / .08); /* hovered, raised */
--shadow-3: 0 8px 24px rgb(0 0 0 / .12); /* dropdown */
--shadow-4: 0 16px 48px rgb(0 0 0 / .18); /* modal */
}Keep a consistent light source. If one shadow is offset downward, all of them are. Mixed shadow directions read as sloppiness even to people who cannot say why.
A shadow is a semantic signal, not a decoration. Reserve the deeper levels for things that genuinely float and are dismissible. A static card with a modal-sized shadow makes the whole hierarchy unreadable.
Elevation does not translate to dark mode. A black shadow on a near-black surface is invisible, so depth in dark themes comes from making the raised surface lighter than its ground. That is one more reason to express elevation as a token with a per-theme value rather than a hard-coded shadow.
Prefer a border to a shadow for anything static, and prefer a background change to a border where you can get away with it. Borders at full strength box the page in; a border at low contrast (a neutral-200 rather than neutral-400) separates without shouting.
8. Icons
Icons are a compression format. They save space and speed recognition for concepts the user already knows, and they hide functionality for everything else.
- Use one icon set. Mixed stroke weights and corner radii between sets are immediately visible and look like a mistake. Pick one (Lucide, Phosphor, Material Symbols, Heroicons) and stay in it.
- Label anything ambiguous. Only a handful of icons are genuinely universal: close, search, print, plus a few platform conventions. A three-dot menu, a bookmark, a share glyph and anything domain-specific need words. Icon-only toolbars are a recognised usability problem, not a modern aesthetic.
- If it must be icon-only, give it an accessible name and a tooltip.
aria-labelfor assistive technology and a visible tooltip on hover and focus for everyone else. - Match the optical size to the text, typically 1 em to 1.25 em beside body text, and align to the cap height rather than the line box.
- Prefer inline SVG for anything that must inherit colour or change with state.
currentColormakes an icon follow its text automatically, which is why every SVG in this notebook uses it. - Decorative icons get
aria-hidden="true", so a screen reader does not announce a meaningless graphic next to the label it duplicates.
<!-- Icon plus text: the icon is decorative, the text is the name -->
<button><svg aria-hidden="true" width="16" height="16">...</svg> Delete site</button>
<!-- Icon only: the icon must carry the name -->
<button aria-label="Delete site" title="Delete site"><svg aria-hidden="true">...</svg></button>SVG covers the format itself in depth.
9. How visual design fails
- No hierarchy. Everything at 14 px in the same grey. Squint at the screen and nothing emerges.
- Too many type sizes and weights, usually because each screen was designed alone. Six sizes that differ by 1 px each communicate less than three that differ clearly.
- Arbitrary spacing. 13 px here, 18 px there, and grouping that has to be rescued with borders.
- Colour as decoration. Accent colour on six elements, so the primary action no longer stands out.
- Colour as the only signal. Red and green status dots with no icon or label.
- Contrast traded for aesthetics. Light grey placeholder text on white is the most common accessibility failure in visual design, and it fails for everyone on a phone outdoors, not only for people with low vision.
- Shadows everywhere. When every element floats, nothing does.
- Centred body text, which forces the eye to find a new left edge on every line.
- Full-width text on a large monitor. A 200-character line is unreadable no matter how good the type is.
- Dark mode by inversion, producing vibrating saturated colours and invisible elevation.
Where this goes next
- Design Systems and Tokens turns every scale in this notebook into named values a whole team can use, and is how light and dark stay in step.
- Responsive and Multiplatform covers how these layouts behave as the viewport changes.
- Accessibility holds the contrast requirements, target sizes and colour independence rules this notebook keeps pointing at.
- Content Design and Motion covers the words inside the hierarchy.
On the implementation side: CSS for the properties, Bootstrap for a ready-made grid and component set, and SVG for vector assets and icons.