jig-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +115 -0
- package/dist/index.js +544 -0
- package/package.json +61 -0
- package/rules/00-anti-patterns.md +501 -0
- package/rules/01-modes.md +213 -0
- package/rules/02-tokens.md +210 -0
- package/rules/03-patterns.md +459 -0
- package/rules/04-principles.md +154 -0
- package/rules/05-copy.md +153 -0
- package/rules.index.json +637 -0
- package/templates/SKILL.md.tmpl +53 -0
- package/templates/command-metadata.json +27 -0
- package/tokens/brand.default.css +152 -0
- package/tokens/mode.editorial.css +72 -0
- package/tokens/mode.operator.css +74 -0
- package/tokens/mode.product.css +69 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# 03 · Patterns
|
|
2
|
+
|
|
3
|
+
**Status:** draft v0.1
|
|
4
|
+
**Depends on:** `00-anti-patterns.md`, `01-modes.md`, `02-tokens.md`
|
|
5
|
+
**Framework:** agnostic. Anatomy and behaviour, not implementation.
|
|
6
|
+
|
|
7
|
+
Each pattern states its **anatomy** (parts, in order), its **states**, its **rules**, and **what varies by mode**. Build the anatomy completely before styling anything. A pattern missing a state is not finished, however good it looks.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## P-01 · Feedback placement
|
|
12
|
+
|
|
13
|
+
Read this before building any pattern that reports something to the user. The most common error in generated UI is not bad styling, it is putting a message in the wrong place — usually a toast, because toasts are easy.
|
|
14
|
+
|
|
15
|
+
| The message is… | Goes | Never |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| About one field | Inline, adjacent to that field, persistent | A toast |
|
|
18
|
+
| About one submission, blocking it | Summary above the form **and** inline per field | A toast |
|
|
19
|
+
| Result of an action the user just took, non-blocking | Toast, 4–6s, dismissible | A dialog |
|
|
20
|
+
| About the whole page or account state | Banner at the top of the content region, persistent | A toast |
|
|
21
|
+
| Requiring a decision before continuing | Dialog | A banner |
|
|
22
|
+
| A background failure the user did not cause | Banner, persistent, with a retry | A toast |
|
|
23
|
+
|
|
24
|
+
**Rules**
|
|
25
|
+
- A toast is for something already done. If the user must act, it is not a toast.
|
|
26
|
+
- Anything a user might need to re-read is not a toast. Errors are re-read.
|
|
27
|
+
- Never stack more than one toast. Replace, or aggregate into a banner.
|
|
28
|
+
- Every error message says what happened **and** what to do next (`F-37`). "Something went wrong" is not a message, it is an apology.
|
|
29
|
+
- Errors are announced to assistive technology: `role="alert"` for immediate, `aria-live="polite"` for summaries (`E-35`).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## P-02 · Button
|
|
34
|
+
|
|
35
|
+
**Anatomy:** `[icon] label [icon]` in a control of height `--size-control`, with a hit area of at least `--size-touch-target` (48px).
|
|
36
|
+
|
|
37
|
+
### Three weights
|
|
38
|
+
|
|
39
|
+
| Weight | Treatment | Use |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| **Primary** | Solid `--color-brand` fill, `--color-brand-on` text, `--radius-control` | The one action the view exists for |
|
|
42
|
+
| **Secondary** | Transparent fill, `--color-brand` border **and** text, same radius and height | The alternative, or several actions of equal weight |
|
|
43
|
+
| **Tertiary** | Transparent, no border, `--color-brand` **underlined** text | Least important actions, repeated actions, destructive actions |
|
|
44
|
+
|
|
45
|
+
**The tertiary underline is not optional.** Without it, colour is the only thing distinguishing the button from plain text, which fails every colour-blind user. Proximity to other buttons may rescue it sometimes; that is not a control you can rely on.
|
|
46
|
+
|
|
47
|
+
### The accessibility floors
|
|
48
|
+
|
|
49
|
+
Four numbers, and most button designs in the wild fail at least one:
|
|
50
|
+
|
|
51
|
+
| Requirement | Threshold |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| Button **shape** — fill or border — against its background | **3:1** |
|
|
54
|
+
| Button **text** against the button | **4.5:1** |
|
|
55
|
+
| Two buttons sharing a style, distinguished only by contrast | **3:1 between them** |
|
|
56
|
+
| Hit area | **48×48px** |
|
|
57
|
+
|
|
58
|
+
A secondary button's fill or border is **not decorative**. It is the only thing identifying the element as a button, so it carries the 3:1 non-text requirement (`02-tokens.md`). Strip it and you have coloured text.
|
|
59
|
+
|
|
60
|
+
### Rules
|
|
61
|
+
|
|
62
|
+
- **Hierarchy must not depend on colour.** Two buttons differing only in hue are identical to a colour-blind user, and if their contrast against each other is under 3:1 they are identical to a low-vision user too. Vary **fill, border and underline** — structure, not just colour.
|
|
63
|
+
- **One primary per view.** Where several actions repeat down a list, they are all secondary or all tertiary; a column of primaries says every row is the most important thing on screen.
|
|
64
|
+
- **Equal importance means equal prominence.** "Report" and "Don't report" are a genuine choice, so both are secondary. Making one primary applies a thumb to the scale.
|
|
65
|
+
- **Never a light grey secondary.** It reads as disabled, and its text and border rarely clear their floors.
|
|
66
|
+
- **Never a second solid fill** in another colour beside the primary. Two solid buttons compete, and the hierarchy collapses.
|
|
67
|
+
- **One shape for all weights.** If the primary is a rounded rectangle, so is the secondary. A pill beside a rectangle implies a difference in function that does not exist.
|
|
68
|
+
- **Label is verb + noun**: "Save post", "Delete invoice", "Add domain". Never "OK", "Submit", "Yes". Buttons are read out of context by screen reader users and by anyone scanning, so the label must work alone.
|
|
69
|
+
- **16px minimum between adjacent buttons**, so nobody hits the wrong one.
|
|
70
|
+
- Width does not change between states. A loading button keeps its width, swaps the label for an indicator, and sets `aria-busy`.
|
|
71
|
+
|
|
72
|
+
### Order and alignment
|
|
73
|
+
|
|
74
|
+
**Start-aligned, ordered most to least important.** The eye returns to the left edge moving down a screen; a right-parked primary can be missed entirely on a wide display or by anyone using a screen magnifier; and putting the most-used action first cuts the distance most people travel.
|
|
75
|
+
|
|
76
|
+
- **Mobile:** stack top to bottom in the same order, full width, so either hand reaches them.
|
|
77
|
+
- **Dialogs:** start-aligned, for consistency with every form in the product. Right alignment is defensible — it is the Mac convention and reads as forward momentum — but pick one and hold it everywhere.
|
|
78
|
+
- **Multi-step forms:** primary start-aligned at the bottom; **"Back" as a tertiary button at the top left**, not beside the primary. A prominent Back next to Next gets clicked by mistake and the entered data is gone.
|
|
79
|
+
- **Exception:** a single-field form — search, email capture — may attach the button to the end of the field. It saves space and reinforces that the two belong together.
|
|
80
|
+
|
|
81
|
+
### Icon and text pairs
|
|
82
|
+
|
|
83
|
+
Match the icon's **weight** and **size** to the text it sits with. Where they cannot be matched — the icon set is heavier or larger than the type — bring the icon's **contrast** down instead: `--color-stroke-strong` for the icon against `--color-text-weak` for the label. The pair should read as one unit, with neither shouting over the other.
|
|
84
|
+
|
|
85
|
+
### States
|
|
86
|
+
|
|
87
|
+
All required (`E-28`): `default`, `hover`, `focus-visible`, `active`, `disabled`, `loading`.
|
|
88
|
+
|
|
89
|
+
Transparent layers are the default treatment — no new tokens, and they work on every surface in both modes:
|
|
90
|
+
|
|
91
|
+
| State | Treatment |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| Hover | Layer `--color-state-hover` over the element |
|
|
94
|
+
| Press / active | Layer `--color-state-press` over the element |
|
|
95
|
+
| Focus | Visible outline on `--color-focus`, never a fill change alone |
|
|
96
|
+
| Disabled | `--opacity-disabled` — but see below |
|
|
97
|
+
|
|
98
|
+
Alternatives where a layer is not enough: change the fill from the palette, change elevation (a card lifting on hover), toggle an underline (remove it from a link that has one, add it to a nav item that does not), or move the element a few pixels. Keep motion short and honour `prefers-reduced-motion` (`G-43`). Press usually matches default, since it only needs to differ from hover.
|
|
99
|
+
|
|
100
|
+
### Instead of disabling
|
|
101
|
+
|
|
102
|
+
Disabled buttons give no feedback on press, often fail contrast, and are skipped by keyboard focus — so the user cannot even reach the thing to find out why it is dead. Three better options, in order:
|
|
103
|
+
|
|
104
|
+
1. **Enable and validate on submit.** Let them press it; show what is missing. A person who skipped a field learns that immediately instead of hunting for the reason the button will not work.
|
|
105
|
+
2. **Remove the action** and say why it is unavailable. "Private account — request to follow this person to see their work."
|
|
106
|
+
3. **Keep the button, add a lock icon.** Full contrast, discoverable, obviously gated. Works well for paid features, provided you say how to unlock them.
|
|
107
|
+
|
|
108
|
+
If you must disable: put a message beside the button explaining what is needed, or a tooltip on it, and **keep it keyboard-focusable** so assistive technology can reach the explanation.
|
|
109
|
+
|
|
110
|
+
### Destructive actions
|
|
111
|
+
|
|
112
|
+
Friction scales with severity, and the first lever is prominence.
|
|
113
|
+
|
|
114
|
+
- **At rest, a destructive action is tertiary.** Less prominent, further from the primary action, or disclosed behind something.
|
|
115
|
+
- **Do not colour it red at rest.** Red makes it *more* prominent — the opposite of what friction means. `--color-danger` styling belongs on the **confirming** button inside a confirmation step, where the user has already chosen and needs to understand the weight of it.
|
|
116
|
+
- Destructive actions sit at least `--spacing-stack` from their nearest common neighbour, and confirm. In `operator`, confirmation is typed (`01-modes.md`).
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## P-03 · Form field
|
|
121
|
+
|
|
122
|
+
**Anatomy, in this order:**
|
|
123
|
+
1. `<label>` — always visible, **stacked above** the input (`F-98`)
|
|
124
|
+
2. Required or optional marker, in the label (`F-97`)
|
|
125
|
+
3. Hint text — **above** the input, below the label
|
|
126
|
+
4. Input
|
|
127
|
+
5. Error message — after the input, `role="alert"`
|
|
128
|
+
|
|
129
|
+
**States:** `default`, `focus`, `filled`, `invalid`, `disabled`, `read-only`.
|
|
130
|
+
|
|
131
|
+
### Label
|
|
132
|
+
|
|
133
|
+
- **Above the input, never beside it.** Left-placed labels force the eye to zig-zag; right-aligning them to fix that creates a jagged left edge that is harder to scan; and long labels wrap badly in the narrow column. Stacked above, the label and input are taken in with a single focus.
|
|
134
|
+
- **Close to its input** — `--spacing-label` (4px), against `--spacing-stack` between fields. The label must be visibly closer to its own input than to anything else, or the pairing is ambiguous (`D-25`).
|
|
135
|
+
- **No instructional verbs.** "Email", not "Enter your email" or "Type your email here". The input field already implies the verb.
|
|
136
|
+
- No "my" or "your" (`I-88`).
|
|
137
|
+
|
|
138
|
+
### Required and optional
|
|
139
|
+
|
|
140
|
+
**Mark both.** Marking only optional fields, with "all fields are required unless marked optional" at the top, fails because people scan past instructions — and marking both is an accessibility requirement for screen reader users anyway.
|
|
141
|
+
|
|
142
|
+
| | Marker |
|
|
143
|
+
| --- | --- |
|
|
144
|
+
| Required | `*` with the convention stated at the top, **or** the word "required" |
|
|
145
|
+
| Optional | The word "optional" |
|
|
146
|
+
|
|
147
|
+
The word "required" is the safer of the two: no instruction needed anywhere, no assumption that the asterisk is understood. The asterisk is more concise and lets people scan the count of required fields at a glance. Either is fine; be consistent.
|
|
148
|
+
|
|
149
|
+
**Never colour the asterisk red.** Red means error.
|
|
150
|
+
|
|
151
|
+
You may leave required fields unmarked only when: the product has no optional fields anywhere; the form is short and familiar (login, newsletter signup); the form asks one question per screen with the reason explained; or testing has shown it is unnecessary.
|
|
152
|
+
|
|
153
|
+
### Hints
|
|
154
|
+
|
|
155
|
+
Above the input, not below. Two reasons: a rule about a password's minimum length is useful **before** typing, not after failing; and the space below an input gets covered by autofill menus and on-screen keyboards, so a hint placed there may never be seen.
|
|
156
|
+
|
|
157
|
+
Do not hide a hint in a tooltip if it is needed to complete the field.
|
|
158
|
+
|
|
159
|
+
### Placeholders
|
|
160
|
+
|
|
161
|
+
Not a label (`F-36`). Placeholders vanish on focus, make an empty field look pre-filled and skippable, and are light enough by design that they usually fail contrast.
|
|
162
|
+
|
|
163
|
+
Use one only as a **format example** — `MM / YY` — or in a single-field search box, and then at 4.5:1 with a real accessible label present.
|
|
164
|
+
|
|
165
|
+
### Width
|
|
166
|
+
|
|
167
|
+
**Match the width to the expected input.** A four-character postcode gets a four-character field. Uniform full-width fields look tidy and lie about the input: width is the strongest hint people have about how much is wanted. Where length varies, size for the common case.
|
|
168
|
+
|
|
169
|
+
### Conventional styling
|
|
170
|
+
|
|
171
|
+
Keep the iconic parts (`E-52`): a rectangle with the label above, a square with a tick for checkbox, a circle for radio. If you restyle — enlarging a radio's target area, say — **keep the circle on the left of the label**. Without it, nobody can tell whether one option or several can be chosen.
|
|
172
|
+
|
|
173
|
+
### Other rules
|
|
174
|
+
|
|
175
|
+
- Error text says what to do (`F-37`), signalled by border **and** text **and** `aria-invalid` — never colour alone (`C-20`).
|
|
176
|
+
- `type`, `inputmode`, `autocomplete`, `enterkeyhint` set correctly (`F-40`).
|
|
177
|
+
- Field borders clear **3:1** (`02-tokens.md`). Low-contrast borders are the single most common form defect, and they fail for a sighted user in sunlight as readily as for someone with low vision.
|
|
178
|
+
- Never reformat or truncate a value while it is being typed.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## P-12 · Choosing an input control
|
|
183
|
+
|
|
184
|
+
The control determines the interaction cost before a single pixel is styled. Choose by the shape of the answer, not by what fits.
|
|
185
|
+
|
|
186
|
+
| The answer is… | Use | Not |
|
|
187
|
+
| --- | --- | --- |
|
|
188
|
+
| One of ~10 or fewer options | **Radio buttons**, stacked vertically | A dropdown |
|
|
189
|
+
| One of a long known list (country, product) | **Autocomplete** | A long dropdown |
|
|
190
|
+
| One of a long list the user must browse | **Two dependent fields** (industry → occupation) | One enormous dropdown |
|
|
191
|
+
| A small number, changed in small steps | **Stepper** | A dropdown or free number field |
|
|
192
|
+
| A large or arbitrary number | **Text input** with `inputmode="numeric"` | A stepper |
|
|
193
|
+
| On or off, applied on submit | **Checkbox** | A toggle |
|
|
194
|
+
| On or off, applied immediately | **Toggle switch** | A checkbox |
|
|
195
|
+
| Several of a set | **Checkboxes**, stacked vertically | A multi-select dropdown |
|
|
196
|
+
|
|
197
|
+
**Why dropdowns lose so often:** they cost open, scroll, choose — several precise interactions, punishing with a motor impairment. They look filled when empty, so they get skipped. And their options are hidden, so they cannot be scanned or compared. Use one when space genuinely matters.
|
|
198
|
+
|
|
199
|
+
**Radio buttons and checkboxes stack vertically.** A horizontal row makes it easy to hit the wrong one and harder to see which label belongs to which control.
|
|
200
|
+
|
|
201
|
+
**Autocomplete:** keep suggestions to about ten to avoid choice paralysis, and **bold the differing part** of each suggestion so they can be told apart at a glance. Suitable when people know what they are looking for; not when they need to browse to decide.
|
|
202
|
+
|
|
203
|
+
**Steppers:** `+` and `−`, not up/down arrows or chevrons — arrows read as a dropdown or accordion. Lay the buttons out **horizontally**, so there is space between them and less chance of hitting the wrong one. Each button gets `--size-touch-target`. Not for large changes.
|
|
204
|
+
|
|
205
|
+
**Checkbox vs toggle** is about *when the change applies*, not about looks. A checkbox waits for submit; a toggle takes effect immediately. Label each with what happens when it is **on**.
|
|
206
|
+
|
|
207
|
+
**Positive phrasing** (`F-103`): test a checkbox label by putting "Yes," in front of it. "Yes, allow automatic updates" works. "Yes, don't allow automatic updates" does not.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## P-04 · Form
|
|
212
|
+
|
|
213
|
+
**Anatomy:** heading → intro → required/optional convention → error summary slot → fields grouped by meaning → primary action → secondary action.
|
|
214
|
+
|
|
215
|
+
### Layout
|
|
216
|
+
|
|
217
|
+
- **One column.** It keeps a single downward path, so there is no decision about what to fill next, nothing gets missed, and screen-magnifier users — who see a narrow slice at a time — do not lose a second column entirely.
|
|
218
|
+
- **Exception:** short, genuinely related fields may sit side by side *within* the column's width — expiry date and CVC, city and postcode. They stay inside the single-column bounds, so they avoid the problems above.
|
|
219
|
+
- Group related fields under headings with `<fieldset>`/`<legend>`, spaced by `--spacing-stack`; fields within a group by `--spacing-group`.
|
|
220
|
+
- Ask for the minimum. Every field costs completions, adds a chance of error, and asks someone to hand over information they would rather not.
|
|
221
|
+
- Prefer an **opt-in** to an optional field (`P-11`).
|
|
222
|
+
|
|
223
|
+
### Validation timing (`F-38`)
|
|
224
|
+
|
|
225
|
+
On blur first; on change once a field has already errored, so recovery is immediate; everything on submit, with a summary that receives focus and links to each failed field.
|
|
226
|
+
|
|
227
|
+
### Multi-step
|
|
228
|
+
|
|
229
|
+
Beyond roughly three question groups, split it.
|
|
230
|
+
|
|
231
|
+
- Say up front how long it takes and what they will need.
|
|
232
|
+
- **Group into few, fuller steps** — six steps of five related questions, not thirty steps of one. More steps is more interaction cost, not less.
|
|
233
|
+
- Order **easiest to hardest**, so early progress is quick.
|
|
234
|
+
- Show progress. People push harder as they near the end.
|
|
235
|
+
- **Let them review and change answers before submitting**, then confirm success and say what happens next.
|
|
236
|
+
- Primary action start-aligned; "Back" as a tertiary button at the top left (`P-02`).
|
|
237
|
+
- Each step still submits without JavaScript (`F-41`) — steps are server-tracked positions, not a client-only wizard.
|
|
238
|
+
|
|
239
|
+
### Other rules
|
|
240
|
+
|
|
241
|
+
- Echo every submitted value back on error (`F-39`).
|
|
242
|
+
- The primary action works without JavaScript. Enhancement intercepts; it does not enable.
|
|
243
|
+
- Destructive or irreversible submissions confirm; in `operator`, by typing.
|
|
244
|
+
- Success navigates or updates in place with a persistent confirmation — not a toast that vanishes before it is read.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## P-05 · Empty state
|
|
249
|
+
|
|
250
|
+
Four distinct cases. Collapsing them into one generic "No data" is the failure (`E-30`).
|
|
251
|
+
|
|
252
|
+
| Case | Message | Action |
|
|
253
|
+
| --- | --- | --- |
|
|
254
|
+
| **Never had data** | What this collection is for | The creation action, prominent |
|
|
255
|
+
| **Filtered to nothing** | Which filters are active | Clear filters |
|
|
256
|
+
| **Error loading** | That it failed, and whether it is transient | Retry |
|
|
257
|
+
| **No permission** | That access is restricted, not that it is empty | Who to ask |
|
|
258
|
+
|
|
259
|
+
**Rules**
|
|
260
|
+
- Never show "no results" when the cause was an error. The user will search for something that was there all along.
|
|
261
|
+
- The filtered-empty state repeats the active query so the user can see the typo.
|
|
262
|
+
- Empty is not a full-page illustration in `product` or `operator`. It is a sentence and a button inside the container that would have held the data.
|
|
263
|
+
- Empty state occupies roughly the space the filled state would, or the layout jumps when data arrives.
|
|
264
|
+
|
|
265
|
+
**Mode variance:** illustration is permitted in `product` empty states only, and never in `operator` (`01-modes.md`).
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## P-06 · Data table
|
|
270
|
+
|
|
271
|
+
Primarily an `operator` pattern; the rules apply wherever a table appears.
|
|
272
|
+
|
|
273
|
+
**Anatomy:** toolbar (search, filters, bulk actions, column control) → header row → body rows → footer (count, pagination).
|
|
274
|
+
|
|
275
|
+
**Rules**
|
|
276
|
+
- Row identity is stable across refresh, sort and filter. A row that moves under a click causes the wrong record to be acted on — the most damaging bug this pattern has.
|
|
277
|
+
- Numeric columns are right-aligned and use tabular figures (`--font-numeric`). Text columns are start-aligned. Never centre either.
|
|
278
|
+
- Column headers state units. "Weight" is ambiguous; "Weight (kg)" is not.
|
|
279
|
+
- Truncation is resolvable by click or expand, never by hover alone (`E-31`, `01-modes.md`).
|
|
280
|
+
- Sort state is visible in the header and reflected in the URL, so a view can be shared.
|
|
281
|
+
- Row actions are tertiary buttons, visible without hover in `operator`.
|
|
282
|
+
- Bulk selection appears once single actions exist on more than ~20 rows. Selection survives pagination or clearly states that it does not.
|
|
283
|
+
- Timestamps are absolute and precise, with relative time secondary (`01-modes.md`). "3 hours ago" alone is unusable in an audit context.
|
|
284
|
+
- Loading replaces rows with skeletons of the same height. Never collapse the table to a spinner — the layout jump loses the user's place.
|
|
285
|
+
- Empty follows `P-05`, inside the table body, with the header still visible.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## P-07 · Dialog
|
|
290
|
+
|
|
291
|
+
**Anatomy:** title (`<h2>`) → body → actions, secondary before primary in DOM order.
|
|
292
|
+
|
|
293
|
+
**Rules**
|
|
294
|
+
- Use the platform: `<dialog>` with `showModal()` gives focus trapping, `Escape`, inertness and top-layer stacking for free (`H-48`).
|
|
295
|
+
- Focus moves to the dialog on open — to the first control, or the container if none — and returns to the trigger on close.
|
|
296
|
+
- Dismissible by `Escape` and by backdrop click, **unless** it reports data loss. Then require an explicit choice.
|
|
297
|
+
- Title states the decision, not the category: "Delete three invoices?", not "Confirm".
|
|
298
|
+
- The confirming button names the outcome: "Delete invoices", not "Confirm".
|
|
299
|
+
- Never nest dialogs. If a dialog needs a dialog, the flow is wrong.
|
|
300
|
+
- Never use a dialog for something a page can hold. Dialogs cannot be linked to, bookmarked or reloaded.
|
|
301
|
+
- `--shadow-raised` is used here — this is one of the few places elevation is legitimate.
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## P-08 · Loading
|
|
306
|
+
|
|
307
|
+
Choose by *what is unknown*, not by preference.
|
|
308
|
+
|
|
309
|
+
| Situation | Use |
|
|
310
|
+
| --- | --- |
|
|
311
|
+
| Shape of the result is known | Skeleton matching the real layout |
|
|
312
|
+
| Shape is unknown | Spinner, centred in the region it will fill |
|
|
313
|
+
| Under ~200ms expected | Nothing. A flash of loading is worse than a brief wait |
|
|
314
|
+
| Over ~10s, or a job | Progress with a stage label, and what happens if they leave |
|
|
315
|
+
| User-initiated, reversible, high confidence | Optimistic update with rollback on failure |
|
|
316
|
+
|
|
317
|
+
**Rules**
|
|
318
|
+
- Load into the region that will hold the result, never a full-page overlay for a partial update.
|
|
319
|
+
- Skeletons match the real content's dimensions, or the layout jumps on arrival.
|
|
320
|
+
- A control that triggered loading enters its own loading state (`P-02`) and stays the same width.
|
|
321
|
+
- Never animate a skeleton faster than `--duration-slow`; a pulsing grid is more distracting than a static one.
|
|
322
|
+
- Announce completion with `aria-live` when focus is elsewhere (`E-35`).
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## P-10 · Interaction cost
|
|
327
|
+
|
|
328
|
+
Not a component. A check to run against any flow before calling it finished. See `04-principles.md`, frame 3.
|
|
329
|
+
|
|
330
|
+
**Count** the clicks, scrolls, pointer distance, keystrokes, waits and things the user must remember or look up.
|
|
331
|
+
|
|
332
|
+
**Then reduce:**
|
|
333
|
+
- Keep the action beside the thing it acts on. Distance is cost (Fitts), and a start-aligned action stays visible to screen magnifier users.
|
|
334
|
+
- A control that closes a panel belongs where the control that opened it was, so the pointer or finger does not travel.
|
|
335
|
+
- Give list and menu items a large, **visibly bounded** target. A border showing the full hit area lets the user be less precise, which is faster.
|
|
336
|
+
- Put a slide-out menu's contents at the top, near its trigger, rather than centred for symmetry.
|
|
337
|
+
- Make targets at least `--size-touch-target`. Bigger targets are faster to hit.
|
|
338
|
+
- Cut choices, or promote a recommended subset (Hick). Every extra option slows the decision.
|
|
339
|
+
- Replace multi-step controls with single-step ones — stepper over select, toggle over dropdown, inline edit over a modal.
|
|
340
|
+
- Remove anything competing for attention with the task: banners, autoplay, unsolicited dialogs.
|
|
341
|
+
|
|
342
|
+
### Reducing choice
|
|
343
|
+
|
|
344
|
+
Four techniques, in the order to try them (Hick's law):
|
|
345
|
+
|
|
346
|
+
1. **Remove.** Every option must earn its place. A subscription form asking for first name and company before email is three decisions where one would do — and each field costs completions.
|
|
347
|
+
2. **Group or categorise.** Choosing between four categories then four items is faster than choosing from sixteen. Tabs, filters and sections all do this.
|
|
348
|
+
3. **Break into steps.** One decision at a time. Long forms become multi-step (`P-04`); large navigation becomes levelled menus.
|
|
349
|
+
4. **Recommend.** Where many choices are equivalent, promote the popular ones — suggested searches, a "most chosen" plan, sensible defaults.
|
|
350
|
+
|
|
351
|
+
**Rule:** when a flow changes, state the before and after counts in one line. "3 clicks + 1 scroll → 2 clicks" is a reviewable claim. "Improved the UX" is not.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## P-11 · Progressive disclosure
|
|
356
|
+
|
|
357
|
+
Reveal information as it is needed rather than all at once. Costs an interaction; buys a large reduction in cognitive load. Use it where most users need only part of the content.
|
|
358
|
+
|
|
359
|
+
**Anatomy:** visible summary → labelled control → disclosed content, adjacent and in flow.
|
|
360
|
+
|
|
361
|
+
**Rules**
|
|
362
|
+
- The control is **descriptively labelled**. "Benefits of a custom domain", not "Read more". It must make sense read aloud out of context, because screen readers announce links and buttons in isolation.
|
|
363
|
+
- Disclosed content appears **next to its trigger**, not elsewhere on the page. Content that opens where the user is not looking has not been disclosed.
|
|
364
|
+
- State is visible: the control shows whether the content is open or closed.
|
|
365
|
+
- Use `<details>`/`<summary>` unless you need behaviour they cannot express (`H-48`). Keyboard, screen-reader and no-JS support come free.
|
|
366
|
+
- **Conditional fields are opt-in, not optional.** A "Mobile number" field revealed by ticking "Receive updates by text" is simpler than a permanently visible optional field — people who do not want it never see it, and people who do get a required field with an obvious reason.
|
|
367
|
+
- **Never hide with it what `E-63` requires visible.** Progressive disclosure is for depth, not for labels, selected states or primary actions.
|
|
368
|
+
|
|
369
|
+
**Mode variance:** `operator` discloses least. For daily users, one dense visible view beats a tidy one that hides what they came for.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Layout method
|
|
374
|
+
|
|
375
|
+
Not a component. The procedure for structuring any screen, before styling anything.
|
|
376
|
+
|
|
377
|
+
### Step 1 — Group
|
|
378
|
+
|
|
379
|
+
Four tools, weakest to strongest. Use the weakest that works (`A-67`):
|
|
380
|
+
|
|
381
|
+
| Tool | Principle | Cost |
|
|
382
|
+
| --- | --- | --- |
|
|
383
|
+
| **Continuity** — aligned in a line | The eye follows a continuous line | Free |
|
|
384
|
+
| **Similarity** — same size, shape, colour | Alike things are read as related | Free |
|
|
385
|
+
| **Proximity** — closer together than to anything else | Near things are read as related | Space |
|
|
386
|
+
| **Common region** — a shared container | Strongest cue, and the heaviest | Clutter |
|
|
387
|
+
|
|
388
|
+
Combine them and the container usually becomes unnecessary — a table's rows are already aligned, alike and close. Break continuity deliberately to mark the end of a group, or to interrupt a list with something that is not part of it.
|
|
389
|
+
|
|
390
|
+
### Step 2 — Order by importance
|
|
391
|
+
|
|
392
|
+
Six variables carry hierarchy: **size**, **colour**, **contrast**, **spacing**, **position**, **depth**. The procedure:
|
|
393
|
+
|
|
394
|
+
1. Group related information into sections.
|
|
395
|
+
2. Order the *sections* by importance; important ones first.
|
|
396
|
+
3. Within each section, style each element by its importance — larger, higher contrast, more surrounding space, elevated.
|
|
397
|
+
|
|
398
|
+
Position does more than it looks: people best recall the **first and last** items in a sequence, so a price placed last, beside the primary action, is the one remembered.
|
|
399
|
+
|
|
400
|
+
Give elements *similar* prominence where they should be read as a pair — matching a label's weight to its icon's balances them instead of letting one shout.
|
|
401
|
+
|
|
402
|
+
### Step 3 — Space from the inside out
|
|
403
|
+
|
|
404
|
+
Start at XS on the innermost rectangle and step up moving outward (`D-69`). Between two options, take the larger.
|
|
405
|
+
|
|
406
|
+
### Step 4 — Align to a grid
|
|
407
|
+
|
|
408
|
+
Main containers align to a 12-column grid; small elements *inside* them do not — those use the spacing options.
|
|
409
|
+
|
|
410
|
+
- **Columns** flexible (percentage), 12 on desktop dropping to 4 on mobile.
|
|
411
|
+
- **Gutters** fixed, narrower than columns, and kept empty. `--grid-gutter`.
|
|
412
|
+
- **Margins** keep content off the screen edge, wider on large screens. `--grid-margin`.
|
|
413
|
+
|
|
414
|
+
### Step 5 — The squint test
|
|
415
|
+
|
|
416
|
+
Blur the design, zoom out, or step back. You should still be able to tell what the screen is for and which element matters most. If everything reads at one weight the hierarchy has failed; if elements smear together the white space is too tight.
|
|
417
|
+
|
|
418
|
+
An agent cannot squint, so use the analogue: **if all type were one size and one colour, would the layout still communicate its order?** If the hierarchy depends entirely on type styling, it is too weak.
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## Building modularly
|
|
423
|
+
|
|
424
|
+
Patterns are not built page-first. Build the smallest pieces, then compose.
|
|
425
|
+
|
|
426
|
+
**Start at the smallest screen.** Narrow space forces prioritisation, and the result stays simpler when it widens. Starting wide invites filling the space, and filled space is cognitive load charged to every user. Widening should add breathing room and, only where it earns its place, more content.
|
|
427
|
+
|
|
428
|
+
1. **Primitives** — button, input, avatar, badge, icon. No layout assumptions, no page knowledge.
|
|
429
|
+
2. **Composites** — field (label + input + error), card, table row, empty state. Built only from primitives.
|
|
430
|
+
3. **Templates** — an arrangement of composites that recurs across pages.
|
|
431
|
+
|
|
432
|
+
Two consequences worth stating, because they are what the discipline buys:
|
|
433
|
+
|
|
434
|
+
- A change to a primitive propagates. Fix the button once and every composite inherits it.
|
|
435
|
+
- If a primitive needs a special case to serve one composite, the composite is wrong, not the primitive. Resist adding a variant prop for a single call site.
|
|
436
|
+
|
|
437
|
+
Before writing a new component, check whether it is a composite of things that already exist (`H-45`). Most are.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Adding a pattern
|
|
442
|
+
|
|
443
|
+
A new pattern earns a place here when it has been built three times. Before then it is a component, not a pattern.
|
|
444
|
+
|
|
445
|
+
Each entry states: anatomy in order, complete state list, rules that are decidable, and mode variance. If a rule cannot be checked by looking at the output, it belongs in `04-principles.md`.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Notes for the author (not for the agent)
|
|
450
|
+
|
|
451
|
+
**Where your taste is recorded here:**
|
|
452
|
+
- `P-01` — the whole feedback table is a position. Toasts are over-used because they are easy to build and require no layout decisions; treating them as the narrowest case rather than the default is deliberate.
|
|
453
|
+
- `P-03` help-text-before-control. Contested — many systems put it after. Placing it before means it is read before the user commits to typing, which matters more in forms people fill once.
|
|
454
|
+
- `P-04` one-column forms, and the no-JS baseline for the primary action.
|
|
455
|
+
- `P-06` stable row identity, absolute timestamps, no hover-only truncation. The procurement instinct again: the record is evidence before it is a convenience.
|
|
456
|
+
|
|
457
|
+
**Deliberately absent.** Navigation, cards, tabs, and toasts-as-a-component. Navigation and cards vary too much by project to have decidable rules yet — they would produce prose, not constraints. Add them once you have built enough to see the invariant.
|
|
458
|
+
|
|
459
|
+
**Worth testing before extending.** These eight cover most of what generated UI gets wrong. Point an agent at a form and a table with `00`, `01`, `02` and `03` loaded, and compare against the same task with nothing loaded. If `P-03` and `P-05` do not visibly change the output, the rules are not decidable enough and the fix is more specificity, not more patterns.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# 04 · Principles
|
|
2
|
+
|
|
3
|
+
**Status:** draft v0.2
|
|
4
|
+
**Read when:** no rule covers the situation (Part 1), or two rules conflict (Part 2).
|
|
5
|
+
|
|
6
|
+
Two kinds of principle, doing opposite work.
|
|
7
|
+
|
|
8
|
+
**Part 1 — Frames (five).** Generative. Use these to find the rule that does not exist yet. `00`–`03` cover known failures; these are the method for recognising a new one. Adapted from the foundations in *Practical UI* (Adham Dannaway).
|
|
9
|
+
|
|
10
|
+
**Part 2 — Tiebreakers.** Adjudicative. Use only when two existing rules point in different directions.
|
|
11
|
+
|
|
12
|
+
If you reach for Part 2 often, the rules in `00`–`03` are underspecified and that is where the fix belongs. If you reach for Part 1 often, you are doing novel work, which is correct.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Part 1 · Frames
|
|
17
|
+
|
|
18
|
+
## Frame 1 — Minimise usability risk
|
|
19
|
+
|
|
20
|
+
**Ask: who could struggle with this, and why?**
|
|
21
|
+
|
|
22
|
+
Design decisions are risk decisions. Almost every appealing choice carries a risk that somebody finds it harder to use:
|
|
23
|
+
|
|
24
|
+
| The appealing thing | The risk |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| Thin, light-grey text | Unreadable with low vision, in sunlight, on a poor screen |
|
|
27
|
+
| Icons without labels | Meaning unavailable to anyone who does not know the glyph |
|
|
28
|
+
| Coloured heading text | Reads as a link; invites a click that does nothing |
|
|
29
|
+
| A subtle, borderless control | Not recognised as interactive |
|
|
30
|
+
| Placeholder instead of label | Disappears exactly when it is needed |
|
|
31
|
+
|
|
32
|
+
The risk is rarely to the median user. It falls on people with reduced vision, low computer literacy, limited dexterity or reduced cognitive capacity — and on ordinary users in bad conditions, which is most conditions. You will usually not know who is on the other side, so design for the widest range you can.
|
|
33
|
+
|
|
34
|
+
**Use it like this:** for any decision no rule covers, name who could struggle and why. If the answer is uncomfortable, take the safer option. If the risk is real and recurring, it is a new rule — add it to `00`.
|
|
35
|
+
|
|
36
|
+
**Floor:** WCAG 2.1 level AA. Meeting AA is the starting point, not the achievement.
|
|
37
|
+
|
|
38
|
+
## Frame 2 — Every detail has a reason you can state
|
|
39
|
+
|
|
40
|
+
**Ask: why this way rather than another way?**
|
|
41
|
+
|
|
42
|
+
Some elements are decorative. Everything else should have a rationale you can articulate — not "it looks better", which is an opinion and cannot be discussed, tested, or handed to someone else.
|
|
43
|
+
|
|
44
|
+
This is the test every rule in this system had to pass, and it is why the token files hold resolved numbers rather than ranges. A range defers the decision to whoever reads it next; a number is a decision that can be argued with.
|
|
45
|
+
|
|
46
|
+
**Use it like this:** when you make a call the rules do not cover, state the reason in one line. If you cannot, you are guessing — and a guess should be surfaced as a question, not shipped as a decision (Tiebreaker 5).
|
|
47
|
+
|
|
48
|
+
## Frame 3 — Minimise interaction cost
|
|
49
|
+
|
|
50
|
+
**Ask: what does this cost the user, counted?**
|
|
51
|
+
|
|
52
|
+
Interaction cost is the total physical and mental effort to complete a task: clicks, scrolls, pointer distance, keystrokes, waiting, reading, and anything the user must remember or go and find. Its virtue is that it is **countable**, which makes it the most checkable idea in this file.
|
|
53
|
+
|
|
54
|
+
Three reliable reductions:
|
|
55
|
+
|
|
56
|
+
1. **Keep related actions close, and targets large.** Per Fitts's law, time to hit a target falls as it gets nearer and bigger. Put the action beside the thing it acts on; keep targets at `--size-touch-target` or larger. Start-aligning the action also keeps it visible to screen-magnifier users.
|
|
57
|
+
2. **Cut distraction.** Banners, autoplay, unsolicited dialogs — cost charged against a task the user did not choose to pause.
|
|
58
|
+
3. **Cut choices.** Per Hick's law, decision time rises with the number and complexity of options. Fewer options, or a promoted recommended subset, produces faster decisions.
|
|
59
|
+
|
|
60
|
+
**Use it like this:** count before and after, and state it. "3 clicks + 1 scroll → 2 clicks" is reviewable. "Improved the UX" is not. See `P-10`.
|
|
61
|
+
|
|
62
|
+
## Frame 4 — Minimise cognitive load
|
|
63
|
+
|
|
64
|
+
**Ask: how much thinking does this require that is not the user's actual task?**
|
|
65
|
+
|
|
66
|
+
Attention spent decoding the interface is unavailable for the work. Reliable reductions:
|
|
67
|
+
|
|
68
|
+
- Remove styles, information and decisions that carry no meaning.
|
|
69
|
+
- Break information into smaller groups, so relationships are visible rather than worked out.
|
|
70
|
+
- Use conventional patterns. Familiarity is free comprehension; novelty is charged to the user.
|
|
71
|
+
- Stay consistent — things that look alike must behave alike, or each instance is re-learned.
|
|
72
|
+
- Make hierarchy visible, so importance is seen rather than inferred.
|
|
73
|
+
|
|
74
|
+
**Use it like this:** when something feels heavy but no rule is broken, the load is usually ungrouped information or an unnecessary decision. Split it or remove it. A long form becomes steps; a wide table becomes fewer default columns; six equal options become two recommended and four behind "more".
|
|
75
|
+
|
|
76
|
+
## Frame 5 — Optimise for the common path
|
|
77
|
+
|
|
78
|
+
**Ask: what are most people here to do?**
|
|
79
|
+
|
|
80
|
+
Roughly 80% of effects come from 20% of causes: most users touch a small share of features, most attention lands on a small share of the page, most complaints trace to a few issues. The number is not the point — the asymmetry is.
|
|
81
|
+
|
|
82
|
+
Effort should follow it. Make the common task excellent before making the rare one possible. A checkout that is flawless for the standard order and merely adequate for the edge case beats one that is uniformly mediocre because every case was treated as equal.
|
|
83
|
+
|
|
84
|
+
**Use it like this:** when a design is getting complicated to accommodate a case, ask how often that case occurs. If it is rare, handle it somewhere else — a secondary flow, a support path, a manual step — and keep the common path simple. Complexity added for a rare case is paid for by every user on every visit.
|
|
85
|
+
|
|
86
|
+
**Caution:** this frame prioritises effort, never access. "Most users don't need it" is a valid reason to move a *feature* off the main path. It is never a reason to skip an accessibility requirement — that is Frame 1, and Frame 1 does not yield.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
# Part 2 · Tiebreakers
|
|
91
|
+
|
|
92
|
+
Seven. Each resolves a specific conflict in a specific direction. A principle that does not tell you what to give up is decoration.
|
|
93
|
+
|
|
94
|
+
### 1. Prefer the loud failure
|
|
95
|
+
|
|
96
|
+
**Between silent failure and visible failure, choose visible.**
|
|
97
|
+
|
|
98
|
+
A form that discards a submission and shows success is worse than one that errors. A page serving stale data without saying so is worse than a slow one. Silent failure is the most expensive class of defect, because the cost is paid by someone who never finds out.
|
|
99
|
+
|
|
100
|
+
### 2. Never destroy on suspicion
|
|
101
|
+
|
|
102
|
+
**When the system suspects input is wrong, mark it and hold it. Do not discard it.**
|
|
103
|
+
|
|
104
|
+
Spam scores, validation failures, duplicate detection — all heuristics, all wrong sometimes. Hold the item, record why, let a person decide. Applies equally to the user's typing: never clear a form, drop a draft, or overwrite without a copy.
|
|
105
|
+
|
|
106
|
+
### 3. Recoverable beats correct
|
|
107
|
+
|
|
108
|
+
**Between preventing a mistake and allowing it to be undone, choose undo.**
|
|
109
|
+
|
|
110
|
+
Prevention charges every user friction on every interaction to guard against a rare error. Recovery costs nothing until the error happens. Exception: genuinely irreversible operations, which confirm — and in `operator`, confirm by typing.
|
|
111
|
+
|
|
112
|
+
### 4. Optimise for who is actually there
|
|
113
|
+
|
|
114
|
+
**When density and legibility conflict, decide by the user's real conditions, not by preference.**
|
|
115
|
+
|
|
116
|
+
A first-time visitor on mobile data in bright sun and an operator at a large display for eight hours need opposite things. Mode encodes this. When the mode is genuinely unclear, ask — do not average, because the average serves neither.
|
|
117
|
+
|
|
118
|
+
### 5. Restraint is the default
|
|
119
|
+
|
|
120
|
+
**When a decision has not been made, ship the plainer thing and surface the question.**
|
|
121
|
+
|
|
122
|
+
An invented accent, a decorative animation, a gradient filling an empty space — each is a decision made by default rather than on purpose. Greyscale plus a stated question is a better deliverable than colour plus an unstated assumption.
|
|
123
|
+
|
|
124
|
+
**Method: design in black and white first.** Build the layout, spacing, size and contrast with no colour at all, then introduce colour only where it carries meaning — interactive elements and system states. Starting in greyscale forces hierarchy to work structurally, and whatever colour you then add is doing a job rather than filling a gap. It is also the fastest route to a palette that survives a colour-blind user.
|
|
125
|
+
|
|
126
|
+
**Ceiling: restraint applies to decoration, never to information.** Minimal is not the same as simple. A sparse interface that has dropped labels, selected states or visible actions is harder to use than a busier one that keeps them — it just photographs better. Strip styling freely; never strip the answers to *what is this*, *which one is selected*, and *what can I do next* (`E-63`).
|
|
127
|
+
|
|
128
|
+
### 6. The platform before the framework
|
|
129
|
+
|
|
130
|
+
**When the browser can already do it, use the browser.**
|
|
131
|
+
|
|
132
|
+
`<dialog>`, `<details>`, `position: sticky`, `:has()`, container queries, native form validation, `popover`. Platform features carry accessibility, keyboard handling and state management that a reimplementation gets wrong and then needs maintaining.
|
|
133
|
+
|
|
134
|
+
### 7. Match the codebase before matching this document
|
|
135
|
+
|
|
136
|
+
**When local convention conflicts with these rules, local convention wins.**
|
|
137
|
+
|
|
138
|
+
A codebase with one consistent approach is more maintainable than one with a better approach applied to 30% of it. Note the divergence, raise it, change it deliberately as its own work — not silently, mid-task.
|
|
139
|
+
|
|
140
|
+
**This tiebreaker outranks the other six.** It does not outrank Part 1: a local convention creating a genuine accessibility risk is a defect to raise, not a convention to match.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Notes for the author (not for the agent)
|
|
145
|
+
|
|
146
|
+
**What changed in v0.2.** Part 1 did not exist. The file was adjudicative only — seven tiebreakers that fire when rules collide, with no method for producing a rule not yet written. That meant the system handed an agent 51 known failures and no way to recognise the 52nd. The four frames are that method.
|
|
147
|
+
|
|
148
|
+
Frame 3 is the most immediately useful, because it is the only idea here that produces a number. Everything else in this system is checked by inspection; interaction cost is checked by counting, which makes it the one principle an agent can be held to objectively.
|
|
149
|
+
|
|
150
|
+
Frames 1 and 2 are close to reasoning already embedded in `00` — the risk frame is *why* most of those rules exist, and the rationale requirement is the decidability test that let them in. Stating them explicitly means the next rule can be derived rather than remembered.
|
|
151
|
+
|
|
152
|
+
**Tiebreaker 7 remains the one to argue about**, and now has a stated ceiling: it loses to Frame 1. Without that boundary, "match the codebase" would license inheriting anything.
|
|
153
|
+
|
|
154
|
+
Tiebreakers 1 and 2 are the same instinct from two directions, and both come from outside software — a document that looks wrong gets marked and filed, never destroyed.
|