Craft03/04
Interface details that add up
Most of these details go unnoticed one at a time. Together they decide whether an interface feels careful. When something works exactly as people expect, they carry on without a second thought, and in the aggregate the small details become visible. (STD-visual-details-50)
Each rule below is an OpenDesigner house standard or one of its accessibility floors, with its source named. The drawings use this site's own type and colors, and the measurements in them are real.
Letter spacing and line height change with size
Never use one letter spacing for every size. Tighten large headings, for example to -0.02em. Give small text slightly more space, and leave body text near 0. Letters look further apart as text grows. (STD-visual-details-05)
Give capitals more room. Uppercase labels need looser letter spacing than the same words in sentence case, or they look cramped. (STD-visual-details-04)
Set line height inversely to size. Keep it tight on large headings, for example 1.05, and looser on body text, for example 1.5. (STD-visual-details-06) Build hierarchy from weight, size and line height together, not from size alone. (STD-visual-details-07)
Keep lines of body text to about 65 characters, so they stay comfortable to read. (STD-visual-details-01)
- 1Large text: tighter. Letter spacing -0.02em, line height 1.05. Big letters drift apart, so they are pulled in.
- 2Capitals: looser. Wider than the same words in sentence case. This label uses 0.07em.
- 3Body: neutral. Letter spacing near 0, line height 1.5, lines no wider than about 65 characters.
Source: the apple-design skill in emilkowalski/skills, and Emil Kowalski's essay Agents with taste for capitals and line length.
Numbers that change in place
Use tabular digits for price columns and for numbers that update, like timers and counters. Tabular digits all have the same width. In CSS that is font-variant-numeric: tabular-nums. Columns then line up, and a changing number does not shift as it updates. (STD-visual-details-02)
Digits of different widths
The narrow 1s pull some amounts to the right, so the decimal points no longer line up and the column is hard to scan.
Tabular digits
Every digit takes the same width, so the decimal points sit on one line and a changing number does not jiggle.
When a number animates, such as a counter or a price, use the NumberFlow library instead of re-rendering the text. (STD-visual-details-59)
Source: Emil Kowalski's Agents with taste and the animation-vocabulary and pick-ui-library skills in emilkowalski/skills.
Small habits in text
Write the ellipsis as one character (…), never as three periods. (STD-visual-details-03)
Underline only links. Emphasize other text with weight or color. Then an underline always means "you can click this". (STD-visual-details-09)
Emphasize interface text with weight, not italics. Italics belong to citations and stress in long prose; in an interface they read like print. (STD-visual-details-08)
Print habits in an interface
Three periods instead of an ellipsis, an underline on words that are not a link, and italics for emphasis.
Interface habits
One ellipsis character, weight for emphasis, and underlines kept for links, so an underline always means “you can click this”.
Source: Emil Kowalski's Agents with taste.
Soft edges, not hard lines
Separate surfaces with a semi-transparent shadow rather than a solid, opaque border. A solid border where a shadow belongs is one of the small mistakes that add up to an interface that is not great. (STD-visual-details-14)
A solid, opaque border
A flat, opaque gray line draws the same hard box around each card, whatever sits behind it.
A semi-transparent shadow
A faint ring of the ink color plus a soft drop. The edge is partly see-through, so it blends with whatever is behind it.
box-shadow: 0 0 0 1px rgb(34 36 40 / 0.13), 0 1px 2px rgb(34 36 40 / 0.08), 0 10px 24px -12px rgb(34 36 40 / 0.34);
Under a sticky header, fade the edge instead of drawing a permanent line. Show a short blur or gradient only where content actually passes under the header. (STD-visual-details-15)
A hard line under the header, always
The 1 px divider is there even when nothing is underneath, and it cuts the scrolling content with a sharp edge.
A soft edge where content passes under
The header is translucent and a short fade appears only where content slides beneath it. No permanent line.
Never place one light, see-through surface on another. Text on stacked translucent layers becomes hard to read. (STD-visual-details-18)
Source: the README and the apple-design skill in emilkowalski/skills.
Targets and focus, measured
Make every touch target at least 44 by 44 points on iOS, 48 dp on Android and 44 px on the touch web. When the drawn control is smaller, grow its hit area with a pseudo-element on the web, or hitSlop in React Native, never by growing the drawing. (STD-mobile-touch-09)
No web target, for a mouse or a finger, is smaller than 24 by 24 CSS pixels. This is an accessibility floor, from WCAG 2.2 2.5.8 Target Size (Minimum).
Every focusable element shows a focus ring: 2 px wide, 2 px from the element, with at least 3:1 contrast against what is around it. This is also a floor, from 2.4.7 Focus Visible and 1.4.11 Non-text Contrast.
Set the text in inputs to 16 px or more. Below that, iOS Safari zooms the page when the field gets focus and does not zoom back out. Never turn off pinch zoom to stop it. (STD-mobile-touch-11, STD-accessibility-motion-27)
- 1Icon button. The icon is drawn at 20 px. Its hit area (dashed) grows to 44 px for touch with a pseudo-element, never by growing the icon. 24 by 24 px (green) is the floor for every web target.
- 2Focus ring. 2 px wide, 2 px from the edge, and at least 3:1 against what is around it.
- 3Input text. 16 px or larger, so iOS Safari does not zoom the page when the field gets focus.
Source: the mobile-native, animate-expo and apple-design skills in emilkowalski/skills, and Emil Kowalski's Agents with taste.
Text on colored fills
Text on a fill needs at least 4.5:1 contrast, or 3:1 when it is large. OpenDesigner measures with the WCAG 2 formula and never rounds up, so 4.49:1 fails. This is an accessibility floor, from 1.4.3 Contrast (Minimum).
Put color on solid layers, not on see-through ones. Over a translucent surface, use higher-contrast text, a slightly heavier weight and a little more letter spacing. (STD-visual-details-21, STD-visual-details-13)
Text that sinks into its fill
Dark text on bright cobalt, white on a pale tint, white on mid gray. Measured, none reaches 4.5:1, the floor for body text.
#222428 on #2338ff2.28:1
#ffffff on #e3ecff1.18:1
#ffffff on #8087933.61:1
Each fill with its own text color
Each fill gets a text color chosen for it: white on the strong fill, a dark shade of the same hue on the pale ones. Every pair clears 4.5:1.
#ffffff on #2338ff6.81:1
#1c31ce on #e3ecff7.44:1
#00652c on #ebf7ed6.57:1
Source: the apple-design skill in emilkowalski/skills, for the rules on translucent surfaces.
Build from the tokens
Tokens are the named values of a design system, such as a color or a spacing step. See Design tokens.
Build from the project's existing tokens, and extend them when something is missing. Never hand-type or approximate a value, and never start a second, parallel set. (STD-visual-details-26)
Make every spacing, timing and alignment value a choice you can defend. (STD-visual-details-27) Do not ship jittery scrolling, misaligned icons or layouts that break when the device rotates; they read as carelessness. (STD-visual-details-28)
Source: the prototype, animate and review skills in emilkowalski/skills, and the apple-design skill.
Where these rules come from
The house standards and their sources are listed in the standards file, and the floors on the Accessibility floors page. Who OpenDesigner learns from describes every source. OpenDesigner is not affiliated with or endorsed by the people it cites.