diff --git a/.claude/skills/uxcli/SKILL.md b/.claude/skills/uxcli/SKILL.md index e4c8a6d..d3e5577 100644 --- a/.claude/skills/uxcli/SKILL.md +++ b/.claude/skills/uxcli/SKILL.md @@ -32,6 +32,8 @@ Use this bounded path for ordinary UI work. Read a reference only where a step n needs, the commitments already signed over it, and what the last run saw. Read it top to bottom. If there is no journey or no understanding on disk, the card says so; go to `references/understand.md` and `references/journey.md` before writing any UI. + If you do not know the product's field from sources, research it first: `npx -y @junixlabs/uxcli + template` names a starting point for the kind of product, and `references/research.md` is how. 3. **Draw before you build.** First pick the lens for what the person does on this screen — `marketing`, `content`, `data`, `workspace`, `shop` or `transaction` — and read its checklist, `lenses/.md` in this skill: named designers' rules, each with its source and what to look @@ -60,6 +62,9 @@ Use this bounded path for ordinary UI work. Read a reference only where a step n Read each card. A `fail` names the element, the place, the rule and how to check it yourself; fix that element and run again. Never edit a journey, a commitment or a probe to make a run pass. Then review the built screen through its lens, the same way, and `uxcli review check`. + Then read what the person goes through: `npx -y @junixlabs/uxcli experience --page` — steps, clicks, + typing, scrolls, waits and a keystroke-level estimate per journey, each finding pinned on the step's + screenshot, and what moved since the walk before. Report each finding in its words; they never block. 6. **Say done only at exit 0**, and say what was measured. Anything else, say what stands in the way. `references/before-done.md` is the full sequence and the list of what exit 0 does not cover. @@ -96,6 +101,9 @@ Before you design or change a screen, you read `uxcli context show` for the jour | `references/journey.md` | the card says no journey, or a flow has changed shape: states as signals, hooks, workflows | | `references/mockups.md` | a screen the journey names has no mockup or no pick: variants, hooks, `pick.json` | | `references/understand.md` | the card says no understanding, or an insight the screen leans on is missing or a hypothesis | +| `references/versions.md` | a walk showed something worth changing: naming versions, a redesign proposal with evidence, comparing two versions | +| `references/research.md` | you do not know the product's field from sources yet: where to look, what to come back with, how to write it down | +| `templates/.md` | the product is a kind uxcli has a template for — `workspace`, `shop`, `landing`: screens and their lens, journeys to walk, what to research | | `lenses/.md` | before you draw or change a screen: the checklist for its kind of UI — `marketing`, `content`, `data`, `workspace`, `shop`, `transaction` | | `references/lenses.md` | how to pick the lens, answer it, write the review, and pass `review check` | | `references/principles.md` | a verdict is `not-committed` and the project should decide; proposing a commitment | diff --git a/.claude/skills/uxcli/lenses/content.md b/.claude/skills/uxcli/lenses/content.md index 3b94e01..f93a15b 100644 --- a/.claude/skills/uxcli/lenses/content.md +++ b/.claude/skills/uxcli/lenses/content.md @@ -2,7 +2,7 @@ Docs, articles, blogs and help centres: long text read and navigated by section. -32 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. +39 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. ## 1. `usability.signifiers-make-clickable-look-clickable` @@ -61,7 +61,19 @@ Nothing that is not interactive may look interactive: no underlined or blue stat - **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons - **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). -## 6. `usability.follow-conventions` +## 6. `usability.mindless-clicks-not-fewer-clicks` + +Do not count clicks; make each one an unambiguous choice with clear link text that says where it goes. + +- **Source:** Steve Krug, *Don't Make Me Think, Revisited — chapter 4 (Krug's Second Law of Usability)* — https://ptgmedia.pearsoncmg.com/images/9780321965516/samplepages/0321965515.pdf +- **In their words:** "It doesn't matter how many times I have to click, as long as each click is a mindless, unambiguous choice." +- **Do:** Write link text that identifies its target; Use breadcrumbs and hub pages for scent; Keep clear labelling with strong information scent +- **Don't:** Count clicks as the metric; Use generic links like 'Click here' or 'Learn more' +- **Look at:** Count links and buttons whose text is generic ('Click here', 'Learn more', 'Read more', bare 'Next') or duplicates another link's text with a different target; do not measure path length. +- **Unless:** Fewer clicks matter more when the same path is drilled repeatedly or pages take long to load (Krug) +- **Also stated as:** writing.links-describe-their-destination (Mailchimp). + +## 7. `usability.follow-conventions` Work the way the sites and platforms users already know; a convention beats a locally optimised novelty. @@ -73,7 +85,7 @@ Work the way the sites and platforms users already know; a convention beats a lo - **Unless:** Desktop apps follow their own OS: Windows OK-first, Apple OK-last - **Also stated as:** modern.boring-and-familiar-beats-novel (Scott Berkun). -## 7. `usability.banner-blindness-dont-style-content-like-ads` +## 8. `usability.banner-blindness-dont-style-content-like-ads` Keep essential content and the primary action in the main column, styled like content, never in a right rail, top strip or animated coloured box. @@ -84,7 +96,7 @@ Keep essential content and the primary action in the main column, styled like co - **Look at:** Is the journey's primary action or a required notice positioned in the right rail (x > 70% of desktop viewport) or in a full-width top strip with background fill and animation? - **Unless:** On mobile, large inline ads do get fixated — the effect is weaker for inline placement -## 8. `usability.aesthetic-usability-effect-bias` +## 9. `usability.aesthetic-usability-effect-bias` Polish makes a screen look more usable than it is; discount your aesthetic impression and run the measurable checks first. @@ -95,7 +107,7 @@ Polish makes a screen look more usable than it is; discount your aesthetic impre - **Look at:** Did the evaluator judge the screen usable because it looks good? Re-check the verdict against the count-kind entries before trusting it; 'attractive' is the variable and stays taste. - **Unless:** With severe usability issues, or functionality sacrificed for aesthetics, users lose patience -## 9. `usability.natural-mapping-and-proximity` +## 10. `usability.natural-mapping-and-proximity` Put a control next to the thing it changes and a label next to its field; gaps between groups must exceed gaps within them. @@ -106,17 +118,6 @@ Put a control next to the thing it changes and a label next to its field; gaps b - **Look at:** Distance from each label to its own input versus the nearest other input; gap within a fieldset versus gap between fieldsets; distance from last input to the submit button. - **Unless:** Left-aligned labels are acceptable if space is constrained and labels are of similar length -## 10. `usability.mindless-clicks-not-fewer-clicks` - -Do not count clicks; make each one an unambiguous choice with clear link text that says where it goes. - -- **Source:** Steve Krug, *Don't Make Me Think, Revisited — chapter 4 (Krug's Second Law of Usability)* — https://ptgmedia.pearsoncmg.com/images/9780321965516/samplepages/0321965515.pdf -- **In their words:** "It doesn't matter how many times I have to click, as long as each click is a mindless, unambiguous choice." -- **Do:** Write link text that identifies its target; Use breadcrumbs and hub pages for scent; Keep clear labelling with strong information scent -- **Don't:** Count clicks as the metric; Use generic links like 'Click here' or 'Learn more' -- **Look at:** Count links and buttons whose text is generic ('Click here', 'Learn more', 'Read more', bare 'Next') or duplicates another link's text with a different target; do not measure path length. -- **Unless:** Fewer clicks matter more when the same path is drilled repeatedly or pages take long to load (Krug) - ## 11. `usability.start-with-user-needs-design-with-data` Name the actor and their need before drawing a screen, then let measured behaviour, not hunches or looks, decide what changes. @@ -173,7 +174,19 @@ Body copy at 16px or more with 1.5 line height; inputs at 16px or more on mobile - **Unless:** Interaction-heavy desktop pages may go to 14px; Captions sit 2px under body by design - **Also stated as:** canon.butterick-body-size-15-25px (Matthew Butterick); modern.readable-type-sizes-and-weights (Rauno Freiberg). -## 16. `craft.line-length-50-75-characters` +## 16. `craft.wcag-contrast-and-dont-rely-on-colour-alone` + +Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. + +- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html +- **In their words:** "The WCAG recommended contrast ratio for body text — 4.5:1 to meet AA standards" +- **Do:** Style coloured badges as a soft background with dark text; Pair every colour state with an icon or label +- **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only +- **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. +- **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself +- **Also stated as:** modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group). + +## 17. `craft.line-length-50-75-characters` Cap paragraph measure at 50–75 characters per line and do not let it fall under about 30. @@ -185,7 +198,7 @@ Cap paragraph measure at 50–75 characters per line and do not let it fall unde - **Unless:** Tables and forms are not paragraph text; Blurbs under 3 lines are not measured - **Also stated as:** canon.bringhurst-measure-45-75 (Robert Bringhurst). -## 17. `craft.center-text-only-under-three-lines` +## 18. `craft.center-text-only-under-three-lines` Centre text only when there are fewer than three lines, and rarely even then; body text is left-aligned. @@ -197,18 +210,6 @@ Centre text only when there are fewer than three lines, and rarely even then; bo - **Unless:** Poetry, quotes or hero copy the project explicitly commits to - **Also stated as:** canon.vignelli-flush-left (Massimo Vignelli). -## 18. `craft.wcag-contrast-and-dont-rely-on-colour-alone` - -Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. - -- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html -- **In their words:** "The WCAG recommended contrast ratio for body text — 4.5:1 to meet AA standards" -- **Do:** Style coloured badges as a soft background with dark text; Pair every colour state with an icon or label -- **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only -- **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. -- **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself -- **Also stated as:** modern.contrast-and-not-colour-alone (Apple). - ## 19. `craft.size-isnt-everything-use-weight-and-colour` Build text hierarchy from two or three colours and two weights; use size only for real levels. @@ -275,18 +276,7 @@ The leading is the vertical unit; add and remove vertical space in multiples of - **Look at:** Body line-height L; margins and paddings between text blocks as multiples of L (or L/2 if the lens allows). - **Unless:** More leading for longer measures, darker faces, larger x-height and sans serifs: the ratio moves with the face -## 25. `modern.cards-are-a-choice-not-a-default` - -Cards are for browsing; use lists for searching and tables for comparing, and keep each card readable with one primary action. - -- **Source:** Stan Kirilov, *UI Card Design (StanVision journal)* — https://www.stan.vision/journal/ui-card-design-examples-best-practices-and-common-patterns (secondary) -- **In their words:** "Cards for browsing. Lists for searching. Tables for comparing." -- **Do:** Lists for ranked or homogeneous data, tables for comparison; Consistent card heights; The title as the link, with a pseudo-element extending the hit area -- **Don't:** Card grids for search results and settings; Whole-card anchor wrappers; More than 3 clickable elements per card; Nested cards -- **Look at:** Detect card grids (3+ siblings with equal border, shadow and radius); fail on text under 14px, over 3 interactive descendants per card, row heights differing over 20%, or an anchor wrapping more than heading plus paragraph. -- **Unless:** Masonry where variable height is the design choice; Media browsing such as Netflix or Pinterest - -## 26. `modern.no-junk-drawer-menus-or-unlabeled-icons` +## 25. `modern.no-junk-drawer-menus-or-unlabeled-icons` Icons carry labels, menus are named for what they hold, and no primary feature hides under More or an ellipsis. @@ -298,7 +288,7 @@ Icons carry labels, menus are named for what they hold, and no primary feature h - **Unless:** Universally recognised icons in tight toolbars, though even the hamburger is weaker than designers think - **Also stated as:** usability.dont-make-me-think (Steve Krug). -## 27. `modern.interactions-feel-immediate-under-200ms` +## 26. `modern.interactions-feel-immediate-under-200ms` Interaction transitions run 200ms or less with an ease-out curve so the interface feels immediate. @@ -309,7 +299,7 @@ Interaction transitions run 200ms or less with an ease-out curve so the interfac - **Look at:** Computed transition-duration and animation-duration on elements that change on :hover, :focus, [aria-expanded] or [data-state]; flag any over 300ms, warn over 200ms; ease-in on an entering element fails. - **Unless:** Toasts may run slower with a plain ease on purpose for tone; Large page or scene transitions and decorative loops sit outside the interaction budget; Exit animations can be a bit more relaxed -## 28. `modern.motion-values-proportional-to-trigger` +## 27. `modern.motion-values-proportional-to-trigger` Scale and fade motion starts near its resting size, in proportion to the trigger, never from zero or a heavy squash. @@ -320,7 +310,7 @@ Scale and fade motion starts near its resting size, in proportion to the trigger - **Look at:** Parse @keyframes and WAAPI keyframes on dialogs, popovers and buttons and read the starting scale(); :active transforms below about 0.9 fail. - **Unless:** Elements that genuinely originate from a point, such as a FAB expanding into a sheet, can grow from small -## 29. `modern.animate-only-transform-and-opacity` +## 28. `modern.animate-only-transform-and-opacity` Animate only transform and opacity, listing properties explicitly; never transition all or animate layout properties. @@ -331,7 +321,7 @@ Animate only transform and opacity, listing properties explicitly; never transit - **Look at:** Scan stylesheets and computed transition-property for all and for layout properties; scan @keyframes for width, height, top and left. - **Unless:** Accordion height animation via grid-template-rows or interpolate-size is layout by nature and accepted when it is the only honest way -## 30. `modern.focus-is-visible-and-unobscured` +## 29. `modern.focus-is-visible-and-unobscured` Every focusable element shows a visible focus ring on :focus-visible, and no sticky or fixed element ever covers it. @@ -341,7 +331,7 @@ Every focusable element shows a visible focus ring on :focus-visible, and no sti - **Don't:** outline: none or 0 with no replacement; Focus rings hidden under sticky headers - **Look at:** Tab through every focusable element; diff the focused and unfocused rect or read computed outline and box-shadow under :focus-visible; check the focused rect is not intersected by position fixed or sticky elements above it. -## 31. `modern.numbers-and-text-do-not-shift-layout` +## 30. `modern.numbers-and-text-do-not-shift-layout` Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. @@ -352,7 +342,7 @@ Numbers in columns and timers use tabular figures, images carry dimensions, and - **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. - **Unless:** Prose numbers -## 32. `modern.defaults-are-decisions-you-inherited` +## 31. `modern.defaults-are-decisions-you-inherited` A library or AI default is someone else's decision; commit to one written aesthetic direction instead of inheriting it. @@ -362,3 +352,92 @@ A library or AI default is someone else's decision; commit to one written aesthe - **Don't:** Untouched library defaults; Clean and modern as a brief; The AI-look constellation of purple gradient, Inter, identical cards, glass glow and bounce hover - **Look at:** Score the constellation: purple-to-blue/cyan gradients warn, Inter or Roboto with no display face notes, 3+ identical icon+h3+p cards note, backdrop-filter plus glow notes, gradient text on numerals warns, overshoot cubic-bezier on hover warns. - **Unless:** A purple brand is allowed to be purple; the tell is the constellation and the absence of a decision, not any one colour + +## 32. `color.from-tokens-not-hex` + +Every colour on the page comes from the design system's named tokens or palette functions, never from hex values copied into components. + +- **Source:** GOV.UK Design System (Government Digital Service), *Styles: Colour* — https://design-system.service.gov.uk/styles/colour/ +- **In their words:** "Do not copy the specific hexadecimal (hex) colour values." +- **Do:** Reference colour by role token (brand, text, error, border) rather than by value; Use palette colours (tints and shades of a few families) for supporting elements; Use a functional token only in the context it is designed for +- **Don't:** Hex literals in component styles; Near-duplicate colours (#1d70b8 next to #1d70b9) created by eye-dropping; Using the error token as a general red +- **Look at:** Collect every computed color, background-color, border-*-color, outline-color and fill/stroke of painted elements, and every value of CSS custom properties declared on :root (and on any theme selector). Count distinct painted colours that match no custom-property value (exact RGBA after resolution), and count pairs of painted colours closer than ΔE 2 that are not identical. +- **Unless:** Images, illustrations and embedded third-party widgets; Browser defaults on unstyled native controls; GOV.UK: palette colours (not functional ones) are allowed for illustrations and custom components + +## 33. `color.one-action-colour-apart-from-status` + +Links and primary actions share one interactive colour family, used consistently, and that colour is not the colour of errors, warnings or success. + +- **Source:** IBM Carbon Design System, *Elements: Color, Overview (Color anatomy)* — https://carbondesignsystem.com/elements/color/overview/ +- **In their words:** "The core blue family serves as the primary action color across all IBM products and experiences. Additional colors are used sparingly and purposefully." +- **Do:** One link colour, used only for links; Primary buttons in the brand or action colour; Danger colour reserved for destructive buttons, not for the primary action +- **Don't:** Links in the error red; Primary buttons in several different hues across screens; Static text coloured like links +- **Look at:** Take the computed text colour of every a[href] in running text and the background of every primary (first, filled, or type=submit) button. Count distinct hues among them (30° bins), and count links or primary buttons whose colour is within ΔE 10 of the page's error, warning or success colour. Also count non-interactive text elements painted in the link colour. +- **Unless:** Primer (GitHub) deliberately uses the success role for primary buttons; a system that states such a mapping consistently is following its own rule; Destructive primary actions (Delete account) take the danger colour on purpose; Navigation menus, where position signals the link + +## 34. `color.focus-ring-contrasts-with-its-surroundings` + +The focus indicator contrasts at least 3:1 with whatever it is drawn against: the page background for an outer ring, the component's own colours for an inner one. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast, Relationship with Focus Visible (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." +- **Do:** An outer ring that contrasts with the page background; A two-colour ring (dark and light) that holds on any background; A thick indicator rather than a 1px one +- **Don't:** A yellow outer ring on a white page; A focus border that changes hue inside the component without contrasting with its fill; Focus shown only by a background tint change +- **Look at:** Tab to each focusable control; diff the focused and unfocused screenshots of the control's box padded by a few pixels; for the changed pixels, take their colour and the colour of the unchanged pixels adjacent to them (page background outside, component fill inside). Count controls where no changed region reaches 3:1 against its adjacent colour. page.focus-visible checks only that some pixel changes; this checks that the change can be seen. +- **Unless:** Unmodified browser default focus styles; WCAG does not compare focused and unfocused states with each other; a background-only change is out of scope for 1.4.11 but fails Use of Color + +## 35. `color.few-families-in-proportion` + +Use a few colour families in a deliberate proportion (neutral base dominant, then primary, secondary and a small accent), not an even spread of many hues. + +- **Source:** U.S. Web Design System (GSA), *Design tokens: Theme color tokens* — https://designsystem.digital.gov/design-tokens/color/theme-tokens/ +- **In their words:** "about 60% of your site’s color would be the primary color family, about 30% would be the secondary color family, and about 10% would be the accent color families" +- **Do:** Neutral base for text and most surfaces; One primary family carrying most of the colour, one secondary, a small accent; Additional colours used sparingly and for a purpose (Carbon); Start in black and white, then add colour to support the message (USWDS) +- **Don't:** Five or more saturated hue families at similar weight on one screen; An accent that covers more area than the primary; A new hue introduced for a single component +- **Look at:** Screenshot the page and bucket every non-neutral pixel (HSL saturation above about 15%) by hue into 30° bins, ignoring images and status colours. How many hue families take more than 2% of the coloured area, and does the largest one carry most of it while the smallest (accent) stays near a tenth? +- **Unless:** USWDS: the proportions are for non-base colours; neutral text will usually dominate; Illustration, photography and data visualisation, which need their own palettes; Brands whose identity is multi-hue + +## 36. `writing.sentence-case-ui-text` + +Labels, buttons, headings, tabs and table headers are written in sentence case; capitals only for the first word, proper nouns and acronyms — never title case or all caps for emphasis. + +- **Source:** IBM Carbon Design System, *Content guidelines: Writing style, Capitalization* — https://carbondesignsystem.com/guidelines/content/writing-style/ +- **In their words:** "Use sentence–case capitalization for all UI text elements." +- **Do:** 'First name', 'Email address', 'Save changes'; Refer to a UI element with the capitalization it has in the UI ('the My network page'); Use bold or italic, not capitals, for emphasis +- **Don't:** Title Case Buttons Like 'Save Your Changes'; ALL CAPS text set in the markup (as opposed to a tracked small label styled with text-transform); Capitalizing feature names to mark them as special +- **Look at:** For each button, label, th, tab and h1–h6 with three or more words, take the rendered text (after text-transform) and count those where two or more non-initial words that are not acronyms or in the page's proper-noun list start with a capital (title case), or where the whole text of four or more words is upper case. Count must be 0. +- **Unless:** Proper nouns, product names and acronyms keep their capitals; Mailchimp's own guide uses title case for page titles, menu names and global navigation — a house style, decide one and keep it; Short tracked overline labels in caps are a typographic choice; the four-word floor leaves them alone + +## 37. `writing.dates-numbers-units-for-the-reader` + +Dates spell out the month, numbers are numerals with thousands separators, and units sit a space after their number — formatted in the reader's locale, never as an ambiguous all-numeric date or a raw machine value. + +- **Source:** Shopify Polaris, *Content: Grammar and mechanics, 'Numbers, dates, and currency'* — https://polaris.shopify.com/content/grammar-and-mechanics +- **In their words:** "Use the month’s full name. If there isn’t enough space, use 3-letter abbreviations. Don’t write dates with numerals only." +- **Do:** 'December 11, 2024' or 'Dec 11, 2024' (in the reader's locale order); Numerals, not words: 'You have 5 orders to fulfill'; Thousands separators: '12,000'; A space between number and unit: '3.4 lb', '2 kg'; Currency code after the amount when currencies can be confused: '$10,000 USD'; Format with Intl.DateTimeFormat / Intl.NumberFormat for the user's locale +- **Don't:** All-numeric dates like '12/11/24'; ISO timestamps or epoch values shown raw ('2024-12-11T09:30:00Z'); Ordinals in dates ('January 23rd'); Unit glued to the number ('3.4lb'); Shortened numbers like '12 k' where the exact value matters +- **Look at:** Scan visible text nodes. Count matches of all-numeric dates (\b\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}\b), raw ISO timestamps (\d{4}-\d{2}-\d{2}T\d{2}:), integers of 5+ digits with no separator outside codes/IDs, and numbers glued to a unit (\d(kg|lb|cm|mm|km|mi|ml|oz)\b). Count must be 0. +- **Unless:** Polaris notes these are American English base rules and dates, numbers and measurements should be localized automatically — the target is the reader's locale, not US format; Identifiers, codes, SKUs and years are not quantities and take no separator; Dense data tables may use compact numeric dates if the format is unambiguous for the locale and stated in the column header + +## 38. `data-display.no-tables-for-layout` + +A table is for comparing data in rows and columns, never for arranging content on the page; layout belongs to the grid. + +- **Source:** GOV.UK Design System (Government Digital Service), *Table — When not to use this component* — https://design-system.service.gov.uk/components/table/ +- **In their words:** "Never use the table component to layout content on a page." +- **Do:** CSS grid or flex for page and dashboard layout; A list, cards or a summary list for items that do not share columns; Tables only where every row has the same fields +- **Don't:** A table that positions a sidebar, form or dashboard tiles; Table cells holding headings, paragraphs or whole forms; role=presentation on a table that actually holds tabular data +- **Look at:** Count table elements that have no th and either contain headings, more than one paragraph per cell, form fieldsets or nested tables, or have a single row whose cells hold unrelated content blocks. +- **Unless:** HTML email, where tables are still the only reliable layout tool +- **Also stated as:** modern.cards-are-a-choice-not-a-default (Stan Kirilov). + +## 39. `data-display.header-cells-are-th-with-scope` + +A data table marks its header cells as th and its data cells as td; when headers run both across and down, scope (or id/headers in complex tables) ties each cell to its headers. + +- **Source:** W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.), *Tables Tutorial* — https://www.w3.org/WAI/tutorials/tables/ +- **In their words:** "Header cells must be marked up with , and data cells with to make tables accessible. For more complex tables, explicit associations may be needed using scope, id, and headers attributes." +- **Do:** A thead row of th scope=col in every data table; th scope=row for the first cell when it names the row's item; id and headers attributes when a cell sits under more than one level of header; A caption that names the table +- **Don't:** Header rows built from td styled bold; Div grids that look like tables with no role=table/row/columnheader/cell semantics; One table holding several topics separated by extra rows of th +- **Look at:** Count table (and role=table/grid) elements that have more than one row of data and no th / role=columnheader; count tables with both a header row and a header column whose th lack scope; count tables with no caption, aria-label or aria-labelledby. +- **Unless:** A simple table with one header row needs only th, not scope; scope matters once there are header rows and header columns; Layout tables, which should not exist (see data-display.no-tables-for-layout) diff --git a/.claude/skills/uxcli/lenses/data.md b/.claude/skills/uxcli/lenses/data.md index 567dbe9..277d16b 100644 --- a/.claude/skills/uxcli/lenses/data.md +++ b/.claude/skills/uxcli/lenses/data.md @@ -2,9 +2,21 @@ Dashboards, analytics, monitoring and reports: numbers and charts read again and again. -40 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. +51 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. -## 1. `usability.feedback-within-a-second` +## 1. `usability.visibility-of-system-status` + +Every action with consequences shows the user something changed, as quickly as possible, so they always know what the system is doing. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #1* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "The design should always keep users informed about what is going on, through appropriate feedback within a reasonable amount of time." +- **Do:** Show a visible trace of every state change; Give feedback immediately, or as quickly as possible; Disable and label a control while its action runs +- **Don't:** Submit silently; Change state with no visible trace; Show an action's result only somewhere else +- **Look at:** Drive an action, then diff DOM or pixels over the next frames: did anything visibly change within 1 s; is there a live region, status text, spinner or busy state? +- **Unless:** Below 0.1 s no special feedback is needed beyond showing the result +- **Also stated as:** modern.feedback-is-local-and-optimistic (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn); writing.success-names-what-happened (Shopify Polaris). + +## 2. `usability.feedback-within-a-second` Paint something within 0.1 s; keep the user's flow with a response under 1 s; show a progress indicator for anything longer. @@ -14,9 +26,9 @@ Paint something within 0.1 s; keep the user's flow with a response under 1 s; sh - **Don't:** Leave dead time with no indicator; Show a percent-done bar that lies badly - **Look at:** Time from input event to first paint of any change; presence of a progress element, aria-busy, or role=status update when the wait exceeds 1 s. - **Unless:** Laws of UX claims a purposeful delay can raise perceived value — an opinion with no study cited -- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn). +- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg). -## 2. `usability.signifiers-make-clickable-look-clickable` +## 3. `usability.signifiers-make-clickable-look-clickable` Links and buttons must look clickable — colour, underline, border or fill — because weak signifiers cost measured time and fixations. @@ -28,7 +40,7 @@ Links and buttons must look clickable — colour, underline, border or fill — - **Unless:** Link position in nav menus or peripheral lists may eliminate the need for underlining (Loranger); Flat works best with low information density, traditional layouts and high-contrast targets positioned standardly (Moran) - **Also stated as:** modern.signifiers-survive-flatness (Kate Moran (NN/G)); modern.no-dead-zones-and-honest-clickability (Rauno Freiberg). -## 3. `usability.fitts-target-size-and-distance` +## 4. `usability.fitts-target-size-and-distance` Make targets big, space them apart, and put them where the pointer already is; touch targets at least 1 cm square. @@ -40,18 +52,6 @@ Make targets big, space them apart, and put them where the pointer already is; t - **Unless:** Infinite targets along screen edges for mouse — size matters less at an edge; Primary CTAs, moving users, children and the elderly need larger than the minimum - **Also stated as:** craft.tap-targets-and-control-height (Erik D. Kennedy); modern.hit-targets-meet-platform-minimums (Apple). -## 4. `usability.visibility-of-system-status` - -Every action with consequences shows the user something changed, as quickly as possible, so they always know what the system is doing. - -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #1* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "The design should always keep users informed about what is going on, through appropriate feedback within a reasonable amount of time." -- **Do:** Show a visible trace of every state change; Give feedback immediately, or as quickly as possible; Disable and label a control while its action runs -- **Don't:** Submit silently; Change state with no visible trace; Show an action's result only somewhere else -- **Look at:** Drive an action, then diff DOM or pixels over the next frames: did anything visibly change within 1 s; is there a live region, status text, spinner or busy state? -- **Unless:** Below 0.1 s no special feedback is needed beyond showing the result -- **Also stated as:** modern.feedback-is-local-and-optimistic (Rauno Freiberg). - ## 5. `usability.speak-the-users-language` Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. @@ -62,8 +62,33 @@ Write in the words the actor already uses; no internal jargon, error codes or un - **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations - **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. - **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se +- **Also stated as:** writing.errors-say-what-and-how-to-fix (GOV.UK Design System (Government Digital Service)). + +## 6. `usability.omit-needless-words` + +Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. + +- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) +- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" +- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns +- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case +- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. +- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text +- **Also stated as:** writing.sentence-case-ui-text (IBM Carbon Design System). + +## 7. `usability.clearly-marked-emergency-exit` + +Every interaction has a visible, labelled way out — Cancel, Back, Escape, Undo — so a mistaken action does not trap the user. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #3* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "Users often perform actions by mistake. They need a clearly marked 'emergency exit' to leave the unwanted action without having to go through an extended process." +- **Do:** Support Undo and Redo; Show a clear Cancel or close control; Label the exit clearly and make it discoverable +- **Don't:** Open a modal with no close; Build a wizard step with no back; Ship a destructive action with no undo +- **Look at:** For each role=dialog or modal: a focusable control whose text or aria-label matches close/cancel/back, and Escape dismisses it; for each step past the first in a flow: a back control exists. +- **Unless:** Legally required interstitials — the exit must still be visible, not necessarily free of consequence +- **Also stated as:** writing.destructive-actions-name-the-consequence (IBM Carbon Design System). -## 6. `usability.no-false-affordances` +## 8. `usability.no-false-affordances` Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. @@ -73,7 +98,7 @@ Nothing that is not interactive may look interactive: no underlined or blue stat - **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons - **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). -## 7. `usability.natural-mapping-and-proximity` +## 9. `usability.natural-mapping-and-proximity` Put a control next to the thing it changes and a label next to its field; gaps between groups must exceed gaps within them. @@ -85,18 +110,7 @@ Put a control next to the thing it changes and a label next to its field; gaps b - **Unless:** Left-aligned labels are acceptable if space is constrained and labels are of similar length - **Also stated as:** craft.law-of-locality (Erik D. Kennedy). -## 8. `usability.omit-needless-words` - -Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. - -- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) -- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" -- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns -- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case -- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. -- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text - -## 9. `usability.aesthetic-usability-effect-bias` +## 10. `usability.aesthetic-usability-effect-bias` Polish makes a screen look more usable than it is; discount your aesthetic impression and run the measurable checks first. @@ -107,17 +121,6 @@ Polish makes a screen look more usable than it is; discount your aesthetic impre - **Look at:** Did the evaluator judge the screen usable because it looks good? Re-check the verdict against the count-kind entries before trusting it; 'attractive' is the variable and stays taste. - **Unless:** With severe usability issues, or functionality sacrificed for aesthetics, users lose patience -## 10. `usability.clearly-marked-emergency-exit` - -Every interaction has a visible, labelled way out — Cancel, Back, Escape, Undo — so a mistaken action does not trap the user. - -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #3* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "Users often perform actions by mistake. They need a clearly marked 'emergency exit' to leave the unwanted action without having to go through an extended process." -- **Do:** Support Undo and Redo; Show a clear Cancel or close control; Label the exit clearly and make it discoverable -- **Don't:** Open a modal with no close; Build a wizard step with no back; Ship a destructive action with no undo -- **Look at:** For each role=dialog or modal: a focusable control whose text or aria-label matches close/cancel/back, and Escape dismisses it; for each step past the first in a flow: a back control exists. -- **Unless:** Legally required interstitials — the exit must still be visible, not necessarily free of consequence - ## 11. `usability.start-with-user-needs-design-with-data` Name the actor and their need before drawing a screen, then let measured behaviour, not hunches or looks, decide what changes. @@ -140,7 +143,19 @@ Separate elements with space, a background shift, a shadow or striping before re - **Unless:** Form inputs and the one object a person acts on keep their border; Keylines that make disconnected content feel connected; Decorative borders as style once the count is low - **Also stated as:** canon.gestalt-common-region (Aurora Harley, NN/g); canon.rams-as-little-design-as-possible (Dieter Rams); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); canon.tufte-smallest-effective-difference (Edward Tufte); canon.tufte-one-plus-one-equals-three (Edward Tufte). -## 13. `craft.spacing-and-sizing-system` +## 13. `craft.dont-overlook-empty-states-teach-by-example` + +Design the first-load and empty states with sample data or an example, a message and a call to action; never a blank page. + +- **Source:** Erik D. Kennedy, *4 Rules for Intuitive UX* — https://www.learnui.design/blog/4-rules-intuitive-ux.html +- **In their words:** "Use the 'first load' experience to provide sample data, showing by example what the properly-working app will look like" +- **Do:** Show a designed empty state with message, example and call to action; Seed sample data on first load; Show examples rather than descriptions +- **Don't:** Show a totally blank page on first load; Show a table header with no rows and no message +- **Look at:** In the journey's declared empty state, find list or table containers with 0 data children; require a visible sentence inside or adjacent and at least one actionable element. +- **Unless:** Zero-hit search results need a message but no sample data; Transient loading states +- **Also stated as:** modern.every-state-is-designed (Vercel Labs); writing.empty-states-say-what-next (GitHub Primer); data-display.empty-data-explains-and-offers-next-step (IBM Carbon Design System); data-display.loading-skeleton-holds-the-layout (GitHub Primer); data-display.wrap-before-truncating-and-reveal-the-rest (GitHub Primer). + +## 14. `craft.spacing-and-sizing-system` Draw every margin, padding and gap from one scale, and make space between groups clearly larger than space within them. @@ -152,7 +167,19 @@ Draw every margin, padding and gap from one scale, and make space between groups - **Unless:** Optical adjustments on icons and hanging punctuation sit a pixel or two off-scale - **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen); modern.spacing-comes-from-a-scale (Stan Kirilov). -## 14. `craft.separation-order-space-then-lines-then-boxes` +## 15. `craft.wcag-contrast-and-dont-rely-on-colour-alone` + +Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. + +- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html +- **In their words:** "The WCAG recommended contrast ratio for body text — 4.5:1 to meet AA standards" +- **Do:** Style coloured badges as a soft background with dark text; Pair every colour state with an icon or label +- **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only +- **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. +- **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself +- **Also stated as:** modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group); color.chart-marks-readable-without-hue (GitHub Primer). + +## 16. `craft.separation-order-space-then-lines-then-boxes` Use the lightest separator that works: more space first, then a keyline or background band, and a box only for the object that is acted on. @@ -162,7 +189,19 @@ Use the lightest separator that works: more space first, then a keyline or backg - **Look at:** Between sibling groups, record which separator is used: gap at least 2x the inner gap (space), hr or border-bottom (line), bordered or shadowed wrapper (box); report the inner/outer gap ratio. - **Unless:** Dense data such as tables where zebra stripes or keylines are used; Interactive cards that are the unit of action -## 15. `craft.de-emphasize-to-emphasize-up-pop-down-pop` +## 17. `craft.align-with-readability-in-mind` + +Keep few strong left edges, right-align comparable numbers with tabular figures, and hang punctuation so edges line up. + +- **Source:** Steve Schoger, *Little UI Details (tweet, 15 Jun 2017)* — https://digitalsynopsis.com/design/useful-ui-ux-design-tips/ +- **In their words:** "Aligning text is an easy way to clean up your design and make your content much more scannable." +- **Do:** Keep few, strong left edges; Right-align numeric columns with tabular figures; Hang bullets, icons and punctuation so text edges align +- **Don't:** Align a right-aligned image to ragged left-aligned text; Centre columns of numbers +- **Look at:** Count distinct left-edge x positions of text blocks in a container at 1px tolerance; for td cells matching a number or currency pattern, text-align must be right or end and tabular figures must be set. +- **Unless:** IDs, ZIP codes and phone numbers stay left-aligned; Centred layouts under 3 lines +- **Also stated as:** data-display.right-align-numbers-tabular-figures (GitHub Primer); data-display.text-left-headers-follow-their-column (W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.)). + +## 18. `craft.de-emphasize-to-emphasize-up-pop-down-pop` To emphasize, quiet the competitors as much as you loudify the hero; never stack every up-pop property on non-title elements. @@ -173,31 +212,7 @@ To emphasize, quiet the competitors as much as you loudify the hero; never stack - **Look at:** Flag text at 2x body size or more that is also font-weight 700+, full contrast and uppercase, when it is not the page h1. - **Unless:** The page title may be all-out up-pop -## 16. `craft.wcag-contrast-and-dont-rely-on-colour-alone` - -Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. - -- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html -- **In their words:** "The WCAG recommended contrast ratio for body text — 4.5:1 to meet AA standards" -- **Do:** Style coloured badges as a soft background with dark text; Pair every colour state with an icon or label -- **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only -- **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. -- **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself -- **Also stated as:** modern.contrast-and-not-colour-alone (Apple). - -## 17. `craft.dont-overlook-empty-states-teach-by-example` - -Design the first-load and empty states with sample data or an example, a message and a call to action; never a blank page. - -- **Source:** Erik D. Kennedy, *4 Rules for Intuitive UX* — https://www.learnui.design/blog/4-rules-intuitive-ux.html -- **In their words:** "Use the 'first load' experience to provide sample data, showing by example what the properly-working app will look like" -- **Do:** Show a designed empty state with message, example and call to action; Seed sample data on first load; Show examples rather than descriptions -- **Don't:** Show a totally blank page on first load; Show a table header with no rows and no message -- **Look at:** In the journey's declared empty state, find list or table containers with 0 data children; require a visible sentence inside or adjacent and at least one actionable element. -- **Unless:** Zero-hit search results need a message but no sample data; Transient loading states -- **Also stated as:** modern.every-state-is-designed (Vercel Labs). - -## 18. `craft.hierarchy-is-everything-squint-test` +## 19. `craft.hierarchy-is-everything-squint-test` Squint: the most important thing must catch the eye first and the least important last; one element dominates each screen. @@ -208,7 +223,7 @@ Squint: the most important thing must catch the eye first and the least importan - **Look at:** Given a declared primary action, score every interactive element as area x contrast x font-weight factor; the declared primary must rank first, and no set of buttons may sit within 10% of each other. - **Unless:** Page titles are the only element styled all-out up-pop; A browsing page such as a gallery may have no single most important thing -## 19. `craft.greys-dont-have-to-be-grey-never-use-black` +## 20. `craft.greys-dont-have-to-be-grey-never-use-black` Tint the neutral scale toward the brand hue and never use pure black for text. @@ -219,7 +234,7 @@ Tint the neutral scale toward the brand hue and never use pure black for text. - **Look at:** Count text, border and background colours with saturation 0 and lightness under 15%, and the share of neutral swatches with saturation exactly 0. - **Unless:** It does not always work; worth a trial; High-contrast or accessibility modes and print -## 20. `craft.type-scale-few-font-sizes` +## 21. `craft.type-scale-few-font-sizes` Use about four font sizes from a fixed scale; reuse the default size for body, menus, lists and controls. @@ -230,17 +245,6 @@ Use about four font sizes from a fixed scale; reuse the default size for body, m - **Look at:** Count distinct computed font-size values on visible text per viewport and how many fall outside the declared scale. - **Unless:** Sizes must stay distinguishable, so large text may step many points apart; Marketing pages may add a display size -## 21. `craft.align-with-readability-in-mind` - -Keep few strong left edges, right-align comparable numbers with tabular figures, and hang punctuation so edges line up. - -- **Source:** Steve Schoger, *Little UI Details (tweet, 15 Jun 2017)* — https://digitalsynopsis.com/design/useful-ui-ux-design-tips/ -- **In their words:** "Aligning text is an easy way to clean up your design and make your content much more scannable." -- **Do:** Keep few, strong left edges; Right-align numeric columns with tabular figures; Hang bullets, icons and punctuation so text edges align -- **Don't:** Align a right-aligned image to ragged left-aligned text; Centre columns of numbers -- **Look at:** Count distinct left-edge x positions of text blocks in a container at 1px tolerance; for td cells matching a number or currency pattern, text-align must be right or end and tabular figures must be set. -- **Unless:** IDs, ZIP codes and phone numbers stay left-aligned; Centred layouts under 3 lines - ## 22. `canon.bringhurst-leading-is-a-rhythmic-unit` The leading is the vertical unit; add and remove vertical space in multiples of it. @@ -253,19 +257,7 @@ The leading is the vertical unit; add and remove vertical space in multiples of - **Unless:** More leading for longer measures, darker faces, larger x-height and sans serifs: the ratio moves with the face - **Also stated as:** canon.mb-grid-fields-from-columns-and-lines (Josef Müller-Brockmann). -## 23. `canon.tufte-data-ink-ratio` - -Show the data; erase ink that does not change when the data changes, and ink that repeats it. - -- **Source:** Edward Tufte, *The Visual Display of Quantitative Information (1983), pp.106–121* — https://guypursey.com/blog/202001041530-tufte-principles-visual-display-quantitative-information (secondary) -- **In their words:** "Above all else show the data. Maximize the data-ink ratio. Erase non-data-ink. Erase redundant data-ink. Revise and edit." -- **Do:** Label values directly on marks; Keep gridlines few and lighter than data -- **Don't:** Gridlines heavier than data; Boxed plot areas and redundant axes; Legends that repeat labels; One value said four ways -- **Look at:** In an SVG/canvas chart: non-data elements (gridlines, frames, ticks, legend entries) versus data elements (bars, points, path segments); stroke weight of gridlines versus data marks. -- **Unless:** Maximize the data-ink ratio, within reason; Principles should not be applied rigidly; better to violate any than place graceless marks -- **Also stated as:** canon.tufte-forgo-chartjunk (Edward Tufte). - -## 24. `canon.tufte-small-multiples` +## 23. `canon.tufte-small-multiples` Compare with repeated same-scale panels, not one overloaded chart. @@ -276,18 +268,7 @@ Compare with repeated same-scale panels, not one overloaded chart. - **Look at:** For sibling charts: equal size, equal axis domain (tick labels or scale attributes), same mark encoding. For one chart: number of overlaid series (more than 5 is a candidate). - **Unless:** Different domains are legitimate when the comparison is of shape, not magnitude (inference, not Tufte's text) -## 25. `canon.tufte-graphical-integrity` - -The size of a mark is directly proportional to the number it shows; bars start at zero; one visual dimension per data dimension. - -- **Source:** Edward Tufte, *The Visual Display of Quantitative Information (1983), graphical integrity* — https://guypursey.com/blog/202001041530-tufte-principles-visual-display-quantitative-information (secondary) -- **In their words:** "Show data variation, not design variation." -- **Do:** Bars from a zero baseline; Areas proportional to values -- **Don't:** Truncated bar axes; 3-D bars for 1-D data; Pictograms scaled in both width and height; Cherry-picked ranges -- **Look at:** Bar charts whose y-domain minimum is not 0; linear-fit residual of bar pixel heights against labelled values (lie factor); pictograms scaled in two dimensions. -- **Unless:** Line charts and dot plots may omit zero; the rule is about the representation being proportional - -## 26. `modern.no-junk-drawer-menus-or-unlabeled-icons` +## 24. `modern.no-junk-drawer-menus-or-unlabeled-icons` Icons carry labels, menus are named for what they hold, and no primary feature hides under More or an ellipsis. @@ -297,20 +278,9 @@ Icons carry labels, menus are named for what they hold, and no primary feature h - **Don't:** More, ellipsis or Tools catch-alls holding primary features; Icon-only toolbars without labels; Tooltips on disabled buttons - **Look at:** Icon-only buttons with no text and no aria-label fail; menus triggered by More, …, Tools or Options holding over 5 items or a primary-action label warn; disabled buttons with title or aria-describedby fail. - **Unless:** Universally recognised icons in tight toolbars, though even the hamburger is weaker than designers think -- **Also stated as:** usability.dont-make-me-think (Steve Krug); craft.labels-are-a-last-resort (Steve Schoger (attendee notes by ynotdraw)). +- **Also stated as:** writing.buttons-name-the-action (IBM Carbon Design System); usability.dont-make-me-think (Steve Krug); craft.labels-are-a-last-resort (Steve Schoger (attendee notes by ynotdraw)). -## 27. `modern.cards-are-a-choice-not-a-default` - -Cards are for browsing; use lists for searching and tables for comparing, and keep each card readable with one primary action. - -- **Source:** Stan Kirilov, *UI Card Design (StanVision journal)* — https://www.stan.vision/journal/ui-card-design-examples-best-practices-and-common-patterns (secondary) -- **In their words:** "Cards for browsing. Lists for searching. Tables for comparing." -- **Do:** Lists for ranked or homogeneous data, tables for comparison; Consistent card heights; The title as the link, with a pseudo-element extending the hit area -- **Don't:** Card grids for search results and settings; Whole-card anchor wrappers; More than 3 clickable elements per card; Nested cards -- **Look at:** Detect card grids (3+ siblings with equal border, shadow and radius); fail on text under 14px, over 3 interactive descendants per card, row heights differing over 20%, or an anchor wrapping more than heading plus paragraph. -- **Unless:** Masonry where variable height is the design choice; Media browsing such as Netflix or Pinterest - -## 28. `modern.frequent-actions-do-not-animate` +## 25. `modern.frequent-actions-do-not-animate` Actions used many times a day, and anything keyboard-triggered, appear instantly without an enter animation. @@ -322,7 +292,18 @@ Actions used many times a day, and anything keyboard-triggered, appear instantly - **Unless:** macOS context menus fade out and blink the chosen item; Rare features may be theatrical (delight-impact curve) - **Also stated as:** modern.delight-scales-with-rarity (Benji Taylor). -## 29. `modern.motion-values-proportional-to-trigger` +## 26. `modern.numbers-and-text-do-not-shift-layout` + +Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. + +- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ +- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" +- **Do:** tabular-nums in tables, timers and prices; width and height on every img +- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover +- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. +- **Unless:** Prose numbers + +## 27. `modern.motion-values-proportional-to-trigger` Scale and fade motion starts near its resting size, in proportion to the trigger, never from zero or a heavy squash. @@ -333,7 +314,7 @@ Scale and fade motion starts near its resting size, in proportion to the trigger - **Look at:** Parse @keyframes and WAAPI keyframes on dialogs, popovers and buttons and read the starting scale(); :active transforms below about 0.9 fail. - **Unless:** Elements that genuinely originate from a point, such as a FAB expanding into a sheet, can grow from small -## 30. `modern.animate-only-transform-and-opacity` +## 28. `modern.animate-only-transform-and-opacity` Animate only transform and opacity, listing properties explicitly; never transition all or animate layout properties. @@ -344,7 +325,7 @@ Animate only transform and opacity, listing properties explicitly; never transit - **Look at:** Scan stylesheets and computed transition-property for all and for layout properties; scan @keyframes for width, height, top and left. - **Unless:** Accordion height animation via grid-template-rows or interpolate-size is layout by nature and accepted when it is the only honest way -## 31. `modern.motion-has-an-origin` +## 29. `modern.motion-has-an-origin` A popover or menu animates from the side facing its trigger; set transform-origin where the motion physically starts. @@ -355,7 +336,7 @@ A popover or menu animates from the side facing its trigger; set transform-origi - **Look at:** For each open popover or menu with a known trigger (aria-controls or aria-haspopup), compare the computed transform-origin with the side facing the trigger. - **Unless:** Centred modals have no anchor; a centred origin is right there -## 32. `modern.honour-prefers-reduced-motion` +## 30. `modern.honour-prefers-reduced-motion` Every large motion has a reduced variant under prefers-reduced-motion; fade instead of slide rather than ignoring the setting. @@ -366,7 +347,7 @@ Every large motion has a reduced variant under prefers-reduced-motion; fade inst - **Look at:** Emulate prefers-reduced-motion: reduce and list elements whose computed animation-name or transition-property still includes translations over about 20px or durations above zero. - **Unless:** Motion that is the content, such as a video or a chart drawing, is out of scope -## 33. `modern.animations-are-interruptible` +## 31. `modern.animations-are-interruptible` An open or close animation can be reversed mid-flight by the next input; nothing waits for a transition to finish. @@ -377,7 +358,7 @@ An open or close animation can be reversed mid-flight by the next input; nothing - **Look at:** Open a panel, immediately send the close input, sample the element's bounding box about 50ms later; if it is still growing the animation was not interruptible. Depends on timing tolerance. - **Unless:** Destructive commits that fire only on gesture end are about triggering, not interruptibility -## 34. `modern.readable-type-sizes-and-weights` +## 32. `modern.readable-type-sizes-and-weights` Body text sits at the platform default size, weights stay 400 or heavier, and weight never changes on hover. @@ -388,7 +369,7 @@ Body text sits at the platform default size, weights stay 400 or heavier, and we - **Look at:** Computed font-size and font-weight of every text node at a phone viewport; count distinct font-family stacks and warn above 2. - **Unless:** Captions and legal text may sit at the platform minimum; Display headings may use light weights at large sizes -## 35. `modern.focus-is-visible-and-unobscured` +## 33. `modern.focus-is-visible-and-unobscured` Every focusable element shows a visible focus ring on :focus-visible, and no sticky or fixed element ever covers it. @@ -398,18 +379,7 @@ Every focusable element shows a visible focus ring on :focus-visible, and no sti - **Don't:** outline: none or 0 with no replacement; Focus rings hidden under sticky headers - **Look at:** Tab through every focusable element; diff the focused and unfocused rect or read computed outline and box-shadow under :focus-visible; check the focused rect is not intersected by position fixed or sticky elements above it. -## 36. `modern.numbers-and-text-do-not-shift-layout` - -Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. - -- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ -- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" -- **Do:** tabular-nums in tables, timers and prices; width and height on every img -- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover -- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. -- **Unless:** Prose numbers - -## 37. `modern.shadows-share-one-light-source` +## 34. `modern.shadows-share-one-light-source` All shadows on a page share one light direction and offset ratio, layered and tinted toward the background hue. @@ -420,7 +390,7 @@ All shadows on a page share one light direction and offset ratio, layered and ti - **Look at:** Collect every computed box-shadow; fail when offset signs mix across the page or distinct shadow strings exceed about 6; warn when shadow hue is far from the background hue with alpha above 0.5. - **Unless:** Inset shadows for sunken fields; Glows meant as glows -## 38. `modern.glass-and-blur-earn-their-place` +## 35. `modern.glass-and-blur-earn-their-place` Translucency is for chrome over busy backgrounds with heavy blur and a contrast fallback, never for body content or decoration. @@ -431,7 +401,7 @@ Translucency is for chrome over busy backgrounds with heavy blur and a contrast - **Look at:** For each element with backdrop-filter, sample contained-text contrast against the composited background at three scroll positions; any under 4.5:1 fails; blur under 12px over an image warns; body text over 2 lines inside glass warns. - **Unless:** OS-level materials that honour the user's Reduce Transparency setting -## 39. `modern.boring-and-familiar-beats-novel` +## 36. `modern.boring-and-familiar-beats-novel` Reuse known controls, gestures and semantics; a novel interaction needs a standard alternative and a reason. @@ -442,7 +412,7 @@ Reuse known controls, gestures and semantics; a novel interaction needs a standa - **Look at:** Custom widgets acting as select or menu with no role or keyboard handling fail; wheel or overscroll listeners calling preventDefault on document warn; drag-only affordances with no button alternative fail. - **Unless:** Deliberate departures with a point of view in place before the first component went down -## 40. `modern.defaults-are-decisions-you-inherited` +## 37. `modern.defaults-are-decisions-you-inherited` A library or AI default is someone else's decision; commit to one written aesthetic direction instead of inheriting it. @@ -452,3 +422,161 @@ A library or AI default is someone else's decision; commit to one written aesthe - **Don't:** Untouched library defaults; Clean and modern as a brief; The AI-look constellation of purple gradient, Inter, identical cards, glass glow and bounce hover - **Look at:** Score the constellation: purple-to-blue/cyan gradients warn, Inter or Roboto with no display face notes, 3+ identical icon+h3+p cards note, backdrop-filter plus glow notes, gradient text on numerals warns, overshoot cubic-bezier on hover warns. - **Unless:** A purple brand is allowed to be purple; the tell is the constellation and the absence of a decision, not any one colour + +## 38. `color.from-tokens-not-hex` + +Every colour on the page comes from the design system's named tokens or palette functions, never from hex values copied into components. + +- **Source:** GOV.UK Design System (Government Digital Service), *Styles: Colour* — https://design-system.service.gov.uk/styles/colour/ +- **In their words:** "Do not copy the specific hexadecimal (hex) colour values." +- **Do:** Reference colour by role token (brand, text, error, border) rather than by value; Use palette colours (tints and shades of a few families) for supporting elements; Use a functional token only in the context it is designed for +- **Don't:** Hex literals in component styles; Near-duplicate colours (#1d70b8 next to #1d70b9) created by eye-dropping; Using the error token as a general red +- **Look at:** Collect every computed color, background-color, border-*-color, outline-color and fill/stroke of painted elements, and every value of CSS custom properties declared on :root (and on any theme selector). Count distinct painted colours that match no custom-property value (exact RGBA after resolution), and count pairs of painted colours closer than ΔE 2 that are not identical. +- **Unless:** Images, illustrations and embedded third-party widgets; Browser defaults on unstyled native controls; GOV.UK: palette colours (not functional ones) are allowed for illustrations and custom components +- **Also stated as:** color.mode-aware-tokens-in-every-theme (GitHub Primer). + +## 39. `color.controls-and-graphics-3-to-1` + +The parts that show a control is there and what state it is in (input borders, checkbox boxes and ticks, toggle tracks, icon-only buttons, meaningful chart marks) contrast at least 3:1 with the colours next to them. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "Unless the control is inactive, any visual information provided that is necessary for a user to identify that a control is present and how to operate it must have a minimum 3:1 contrast ratio with the adjacent colors." +- **Do:** Input borders at 3:1 against the background the input sits on, or a filled input background at 3:1; Checkbox ticks and radio dots at 3:1 against the box; Standalone icons at 3:1 against their background; Avoid very thin lines that anti-alias below the nominal ratio +- **Don't:** Pale grey input borders (#ddd on white is about 1.4:1); A selected state shown only by a faint tint; Hover effects that lower a control's contrast with its surroundings +- **Look at:** For each visible input, select, textarea, checkbox, radio, [role=switch] and icon-only button (no visible text): compute the contrast ratio between the colour that identifies it (border colour, or its own background when it has no border, or the icon fill) and the background behind it; count those below 3:1, unrounded. Disabled controls are skipped. +- **Unless:** Inactive (disabled) controls are exempt; A control identified by its visible text needs no contrasting boundary; Logos and decorative graphics; Appearance determined by the browser and not modified by the author + +## 40. `color.status-colours-keep-their-meaning` + +Each status colour has one meaning across the product (critical for errors and blocked actions, warning for what needs attention, success for what went well, info for tips) and is never borrowed for promotion or decoration. + +- **Source:** Shopify Polaris, *Colors: Palettes and roles (Critical, Success)* — https://polaris.shopify.com/design/colors/palettes-and-roles +- **In their words:** "Elements using critical must convey messaging that implies that an action is impossible, blocked, or has resulted in an error." +- **Do:** Map error, warning, success and info to named roles or tokens and use them only in those roles; Reserve the critical red for errors, blocked actions and destructive buttons; Use the info role, not warning or critical, for tips and announcements +- **Don't:** A sale or 'new' badge in the error red; Success green used to entice or to advertise an offer; Warning colour for 'coming soon' or 'under construction' messaging; Two different reds meaning error on different screens +- **Look at:** Find the colour the page uses for error text (an element with role=alert, aria-invalid's described-by message, or a class/token named error/critical/danger) and the success colour likewise. Count painted elements (text, fill or border) whose colour equals that error or success colour, within a ΔE of 3, and that are neither a validation message, an invalid field, a status badge of that meaning, nor a destructive action. +- **Unless:** Brand colours that happen to be red, as long as a separate, distinct error red is used for errors; Data visualisations where a series colour coincides with a status hue but no status is implied (prefer avoiding it) + +## 41. `color.one-action-colour-apart-from-status` + +Links and primary actions share one interactive colour family, used consistently, and that colour is not the colour of errors, warnings or success. + +- **Source:** IBM Carbon Design System, *Elements: Color, Overview (Color anatomy)* — https://carbondesignsystem.com/elements/color/overview/ +- **In their words:** "The core blue family serves as the primary action color across all IBM products and experiences. Additional colors are used sparingly and purposefully." +- **Do:** One link colour, used only for links; Primary buttons in the brand or action colour; Danger colour reserved for destructive buttons, not for the primary action +- **Don't:** Links in the error red; Primary buttons in several different hues across screens; Static text coloured like links +- **Look at:** Take the computed text colour of every a[href] in running text and the background of every primary (first, filled, or type=submit) button. Count distinct hues among them (30° bins), and count links or primary buttons whose colour is within ΔE 10 of the page's error, warning or success colour. Also count non-interactive text elements painted in the link colour. +- **Unless:** Primer (GitHub) deliberately uses the success role for primary buttons; a system that states such a mapping consistently is following its own rule; Destructive primary actions (Delete account) take the danger colour on purpose; Navigation menus, where position signals the link + +## 42. `color.focus-ring-contrasts-with-its-surroundings` + +The focus indicator contrasts at least 3:1 with whatever it is drawn against: the page background for an outer ring, the component's own colours for an inner one. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast, Relationship with Focus Visible (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." +- **Do:** An outer ring that contrasts with the page background; A two-colour ring (dark and light) that holds on any background; A thick indicator rather than a 1px one +- **Don't:** A yellow outer ring on a white page; A focus border that changes hue inside the component without contrasting with its fill; Focus shown only by a background tint change +- **Look at:** Tab to each focusable control; diff the focused and unfocused screenshots of the control's box padded by a few pixels; for the changed pixels, take their colour and the colour of the unchanged pixels adjacent to them (page background outside, component fill inside). Count controls where no changed region reaches 3:1 against its adjacent colour. page.focus-visible checks only that some pixel changes; this checks that the change can be seen. +- **Unless:** Unmodified browser default focus styles; WCAG does not compare focused and unfocused states with each other; a background-only change is out of scope for 1.4.11 but fails Use of Color + +## 43. `writing.one-label-per-action` + +One concept, one word: controls that do the same thing carry the same label everywhere, and controls that do different things never share a label. + +- **Source:** GitHub Primer, *Accessibility guide: Descriptive buttons, 'How to test names'* — https://primer.style/guides/accessibility/descriptive-buttons +- **In their words:** "When buttons perform the same action, they have the same name." +- **Do:** Keep a terminology list of preferred words and words not to use for the product; Pick one verb per action (Delete or Remove, not both for the same thing) and reuse it across pages; Add the object to disambiguate repeated actions ('Remove Apples', 'Remove Pears') +- **Don't:** Synonyms for one action across screens: 'Save' here, 'Update' there, 'Apply' elsewhere; Identical labels for different actions on the same page; Naming the same object two ways ('workspace' and 'project') in one product +- **Look at:** Across the journey's pages, collect (accessible name, action) pairs for buttons and links, where action is the form action/href/handler target. Count names that map to two or more different actions on one page, and actions reached by two or more different names across pages; also flag known synonym pairs present together (save/update/apply, delete/remove, sign in/log in, cart/basket). Count must be 0. +- **Unless:** Delete and Remove may coexist when they mean different things (destroy vs take out of a collection), as Carbon defines them — then each must be used only for its own meaning + +## 44. `writing.dates-numbers-units-for-the-reader` + +Dates spell out the month, numbers are numerals with thousands separators, and units sit a space after their number — formatted in the reader's locale, never as an ambiguous all-numeric date or a raw machine value. + +- **Source:** Shopify Polaris, *Content: Grammar and mechanics, 'Numbers, dates, and currency'* — https://polaris.shopify.com/content/grammar-and-mechanics +- **In their words:** "Use the month’s full name. If there isn’t enough space, use 3-letter abbreviations. Don’t write dates with numerals only." +- **Do:** 'December 11, 2024' or 'Dec 11, 2024' (in the reader's locale order); Numerals, not words: 'You have 5 orders to fulfill'; Thousands separators: '12,000'; A space between number and unit: '3.4 lb', '2 kg'; Currency code after the amount when currencies can be confused: '$10,000 USD'; Format with Intl.DateTimeFormat / Intl.NumberFormat for the user's locale +- **Don't:** All-numeric dates like '12/11/24'; ISO timestamps or epoch values shown raw ('2024-12-11T09:30:00Z'); Ordinals in dates ('January 23rd'); Unit glued to the number ('3.4lb'); Shortened numbers like '12 k' where the exact value matters +- **Look at:** Scan visible text nodes. Count matches of all-numeric dates (\b\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}\b), raw ISO timestamps (\d{4}-\d{2}-\d{2}T\d{2}:), integers of 5+ digits with no separator outside codes/IDs, and numbers glued to a unit (\d(kg|lb|cm|mm|km|mi|ml|oz)\b). Count must be 0. +- **Unless:** Polaris notes these are American English base rules and dates, numbers and measurements should be localized automatically — the target is the reader's locale, not US format; Identifiers, codes, SKUs and years are not quantities and take no separator; Dense data tables may use compact numeric dates if the format is unambiguous for the locale and stated in the column header + +## 45. `data-display.no-tables-for-layout` + +A table is for comparing data in rows and columns, never for arranging content on the page; layout belongs to the grid. + +- **Source:** GOV.UK Design System (Government Digital Service), *Table — When not to use this component* — https://design-system.service.gov.uk/components/table/ +- **In their words:** "Never use the table component to layout content on a page." +- **Do:** CSS grid or flex for page and dashboard layout; A list, cards or a summary list for items that do not share columns; Tables only where every row has the same fields +- **Don't:** A table that positions a sidebar, form or dashboard tiles; Table cells holding headings, paragraphs or whole forms; role=presentation on a table that actually holds tabular data +- **Look at:** Count table elements that have no th and either contain headings, more than one paragraph per cell, form fieldsets or nested tables, or have a single row whose cells hold unrelated content blocks. +- **Unless:** HTML email, where tables are still the only reliable layout tool +- **Also stated as:** modern.cards-are-a-choice-not-a-default (Stan Kirilov). + +## 46. `data-display.chart-axes-and-bars-are-labelled` + +Quantitative charts label both axes outside the data area in short plain words, and label each bar with what it shows and its value; past about six bars, use a table. + +- **Source:** Shopify Polaris, *Data visualizations — Axis and labelling conventions; Horizontal bar charts* — https://polaris-react.shopify.com/design/data-visualizations +- **In their words:** "All standard charts that show quantitative data have 2 axes that should be labeled for clarity. … Label each bar with what it’s displaying, as well as the value." +- **Do:** Axis titles naming the measure and its unit; Direct labels on or beside each bar, outside the bar when it is too short; Tick labels skipped at regular intervals rather than crammed or slanted; Short standard abbreviations: 'Feb', '10 Apr', '1.2k'; A legend only when there is more than one series (Primer) +- **Don't:** Unlabelled axes or units only in a tooltip; Slanted tick labels squeezed in to fit; A bar chart of dozens of categories where a table would compare them better +- **Look at:** For each chart, count value or category axes with no visible title or unit text, bars or points with no visible label or value (where there are six or fewer), tick labels rotated away from horizontal, and horizontal bar charts with more than six bars. +- **Unless:** Sparklines and small multiples that share labelled axes with a neighbour; Interactive charts where exact values sit in a tooltip, provided the axes still carry titles and units +- **Also stated as:** canon.tufte-data-ink-ratio (Edward Tufte); canon.tufte-forgo-chartjunk (Edward Tufte). + +## 47. `data-display.chart-axes-are-honest` + +Bar and area charts start their value axis at zero; line charts may crop the axis to show change; gaps in the data are shown as gaps, never interpolated. + +- **Source:** IBM Carbon Design System, *Data visualization — Axes and labels* — https://carbondesignsystem.com/data-visualization/axes-and-labels/ +- **In their words:** "Always start numerical axes at zero for part-to-whole and comparisons charts, such as bar and area chart. … Never interpolate between periods when data is unavailable." +- **Do:** A zero baseline on bar, column and area charts; A cropped y-axis only on line and scatter charts, where the trend is the message; Labelled start and end points around a gap in a time series; An explicit axis-break mark when part of an axis is skipped +- **Don't:** Bars rising from a non-zero baseline; A line drawn straight across missing periods; Changing tick increments to hide missing data +- **Look at:** For each bar, column or area chart (SVG or canvas with an accessible data table or chart library config), count those whose value-axis minimum is not 0 for all-positive data; for each time-series line, count runs where a missing period is bridged by a continuous segment. +- **Unless:** Line charts and scatter plots, where Carbon allows a non-zero start because the trend matters more than relative size; Log-scale charts, which have no zero and must say they are log scale +- **Also stated as:** color.chart-palette-fits-the-data (IBM Carbon Design System); canon.tufte-graphical-integrity (Edward Tufte). + +## 48. `data-display.one-unit-and-format-per-column` + +Every value in a column uses the same unit, precision and format; the unit is named once in the header, not repeated in each cell. + +- **Source:** U.S. Web Design System (GSA), *Table — Usability guidance* — https://designsystem.digital.gov/components/table/ +- **In their words:** "Predictably format columns. Take care not to vary units or formatting within the same column. Instead, normalize values so they can be easily compared." +- **Do:** Normalise a column to one unit (all days, not days and weeks); Keep the same number of decimal places in every row of a column; Put the unit or symbol in the column header ('Temperature °C') rather than in every cell; Use one date format and one thousands separator throughout the table +- **Don't:** Mixing '3 days' and '2 weeks' in one column; '12.5', '12.50' and '12.500' in the same column; Abbreviating some values ('1.2k') and not others in the same column +- **Look at:** For each column of numeric td, count distinct decimal-place counts, distinct unit suffixes or prefixes, and distinct date formats; any column with more than one is a failure. +- **Unless:** A column deliberately mixing magnitudes where the unit changes are labelled per cell and sorting is by underlying value — rare, and worth questioning + +## 49. `data-display.header-cells-are-th-with-scope` + +A data table marks its header cells as th and its data cells as td; when headers run both across and down, scope (or id/headers in complex tables) ties each cell to its headers. + +- **Source:** W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.), *Tables Tutorial* — https://www.w3.org/WAI/tutorials/tables/ +- **In their words:** "Header cells must be marked up with , and data cells with to make tables accessible. For more complex tables, explicit associations may be needed using scope, id, and headers attributes." +- **Do:** A thead row of th scope=col in every data table; th scope=row for the first cell when it names the row's item; id and headers attributes when a cell sits under more than one level of header; A caption that names the table +- **Don't:** Header rows built from td styled bold; Div grids that look like tables with no role=table/row/columnheader/cell semantics; One table holding several topics separated by extra rows of th +- **Look at:** Count table (and role=table/grid) elements that have more than one row of data and no th / role=columnheader; count tables with both a header row and a header column whose th lack scope; count tables with no caption, aria-label or aria-labelledby. +- **Unless:** A simple table with one header row needs only th, not scope; scope matters once there are header rows and header columns; Layout tables, which should not exist (see data-display.no-tables-for-layout) + +## 50. `data-display.long-tables-sort-and-say-so` + +Long tables let users sort the columns that have a natural order, start with one column sorted, and expose the sort state to assistive technology. + +- **Source:** U.S. Web Design System (GSA), *Table — Usability guidance* — https://designsystem.digital.gov/components/table/ +- **In their words:** "Enable sort where useful. Add row sorting to individual columns of long tables where the data can be logically ordered either alphabetically or numerically." +- **Do:** Sort controls as buttons inside the th of sortable columns; One column sorted on load, shown with an arrow and aria-sort on its th (Primer); A polite live region that announces the new sort (USWDS); A filter or search field above long tables, and pagination for very large sets +- **Don't:** Long tables with no sort, filter or pagination; A sort state shown only by an icon with no aria-sort; Row sorting on tables with merged (colspan/rowspan) cells +- **Look at:** Count tables with more than 20 body rows (or with pagination) that have no sort control in any th and no filter/search input associated with them; among sortable tables, count those where no th carries aria-sort other than none. +- **Unless:** Short tables a user takes in at a glance; Tables whose row order is itself the meaning (a ranked list, a timetable) may fix the order + +## 51. `data-display.summary-list-for-key-value-facts` + +A set of facts about one thing — label and value pairs — is shown as a summary list (dl with dt and dd), not as a table with no headers or as loose text. + +- **Source:** GOV.UK Design System (Government Digital Service), *Summary list — When to use / When not to use this component* — https://design-system.service.gov.uk/components/summary-list/ +- **In their words:** "Use a summary list to show information as a list of key facts. … only use it to present information that has a key and at least one value." +- **Do:** dl with dt (the key) and dd (the value) for record details, metadata and check-your-answers pages; A row action ('Change') whose accessible name includes the key ('Change name'); Headings or cards to separate several summary lists on one page +- **Don't:** A two-column table without th used to show one record's fields; Key–value pairs set as 'Label: value' runs in a paragraph; A summary list for genuinely tabular data or a plain list of items +- **Look at:** Where the screen shows the fields of a single record (a profile, an order, an item's metadata, a check-answers page), are the label–value pairs marked up as dl/dt/dd with each key visually distinct from its value — and is tabular data comparing several records in a table rather than a summary list? +- **Unless:** Two or three facts inside a card may be inline text if they are not scanned as a set diff --git a/.claude/skills/uxcli/lenses/marketing.md b/.claude/skills/uxcli/lenses/marketing.md index ac48fda..7800363 100644 --- a/.claude/skills/uxcli/lenses/marketing.md +++ b/.claude/skills/uxcli/lenses/marketing.md @@ -2,7 +2,7 @@ Landing, pricing and feature pages: read once, persuade, one call to action. -40 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. +45 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. ## 1. `usability.fitts-target-size-and-distance` @@ -28,17 +28,17 @@ Links and buttons must look clickable — colour, underline, border or fill — - **Unless:** Link position in nav menus or peripheral lists may eliminate the need for underlining (Loranger); Flat works best with low information density, traditional layouts and high-contrast targets positioned standardly (Moran) - **Also stated as:** modern.signifiers-survive-flatness (Kate Moran (NN/G)); modern.no-dead-zones-and-honest-clickability (Rauno Freiberg). -## 3. `usability.visual-hierarchy-for-scanning` +## 3. `usability.omit-needless-words` -Design for scanning, not reading: headings that out-rank body text, bold key phrases, lists, and no walls of unformatted text. +Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. -- **Source:** Kara Pernice, NN/g, *F-Shaped Pattern of Reading on the Web (2017, rev. 2026)* — https://www.nngroup.com/articles/f-shaped-pattern-reading-web-content/ (study) -- **In their words:** "The F-shaped scanning pattern is bad for users and businesses." -- **Do:** Use prominent headings with information-rich opening words; Bold key phrases; employ lists and bullets; Remove unnecessary content -- **Don't:** Leave walls of unstructured text; Let headings fail to out-rank body text visually; Let visual weight contradict importance -- **Look at:** Heading font-size and weight monotonic with level (h1 ≥ h2 ≥ h3 ≥ body); longest paragraph in words; ratio of headings and list items to total text blocks. -- **Unless:** The pattern needs moderate, not high, interest — highly motivated readers read -- **Also stated as:** craft.hierarchy-is-everything-squint-test (Erik D. Kennedy). +- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) +- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" +- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns +- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case +- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. +- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text +- **Also stated as:** writing.sentence-case-ui-text (IBM Carbon Design System); writing.plain-language-reading-level (Shopify Polaris). ## 4. `usability.speak-the-users-language` @@ -61,8 +61,21 @@ Everything on the screen competes with the primary goal; remove what does not se - **Don't:** Let decorative elements distract; Let secondary content outweigh the primary action - **Look at:** At 360 px: count distinct interactive elements above the fold; whether the journey's primary action is within the first viewport; count of elements sharing the accent colour. - **Unless:** Dense expert dashboards where the irreducible information is large +- **Also stated as:** forms.ask-only-what-you-need (GOV.UK Design System). + +## 6. `usability.visual-hierarchy-for-scanning` -## 6. `usability.no-false-affordances` +Design for scanning, not reading: headings that out-rank body text, bold key phrases, lists, and no walls of unformatted text. + +- **Source:** Kara Pernice, NN/g, *F-Shaped Pattern of Reading on the Web (2017, rev. 2026)* — https://www.nngroup.com/articles/f-shaped-pattern-reading-web-content/ (study) +- **In their words:** "The F-shaped scanning pattern is bad for users and businesses." +- **Do:** Use prominent headings with information-rich opening words; Bold key phrases; employ lists and bullets; Remove unnecessary content +- **Don't:** Leave walls of unstructured text; Let headings fail to out-rank body text visually; Let visual weight contradict importance +- **Look at:** Heading font-size and weight monotonic with level (h1 ≥ h2 ≥ h3 ≥ body); longest paragraph in words; ratio of headings and list items to total text blocks. +- **Unless:** The pattern needs moderate, not high, interest — highly motivated readers read +- **Also stated as:** craft.hierarchy-is-everything-squint-test (Erik D. Kennedy). + +## 7. `usability.no-false-affordances` Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. @@ -72,7 +85,19 @@ Nothing that is not interactive may look interactive: no underlined or blue stat - **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons - **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). -## 7. `usability.follow-conventions` +## 8. `usability.mindless-clicks-not-fewer-clicks` + +Do not count clicks; make each one an unambiguous choice with clear link text that says where it goes. + +- **Source:** Steve Krug, *Don't Make Me Think, Revisited — chapter 4 (Krug's Second Law of Usability)* — https://ptgmedia.pearsoncmg.com/images/9780321965516/samplepages/0321965515.pdf +- **In their words:** "It doesn't matter how many times I have to click, as long as each click is a mindless, unambiguous choice." +- **Do:** Write link text that identifies its target; Use breadcrumbs and hub pages for scent; Keep clear labelling with strong information scent +- **Don't:** Count clicks as the metric; Use generic links like 'Click here' or 'Learn more' +- **Look at:** Count links and buttons whose text is generic ('Click here', 'Learn more', 'Read more', bare 'Next') or duplicates another link's text with a different target; do not measure path length. +- **Unless:** Fewer clicks matter more when the same path is drilled repeatedly or pages take long to load (Krug) +- **Also stated as:** writing.links-describe-their-destination (Mailchimp). + +## 9. `usability.follow-conventions` Work the way the sites and platforms users already know; a convention beats a locally optimised novelty. @@ -84,18 +109,7 @@ Work the way the sites and platforms users already know; a convention beats a lo - **Unless:** Desktop apps follow their own OS: Windows OK-first, Apple OK-last - **Also stated as:** modern.boring-and-familiar-beats-novel (Scott Berkun). -## 8. `usability.omit-needless-words` - -Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. - -- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) -- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" -- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns -- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case -- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. -- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text - -## 9. `usability.banner-blindness-dont-style-content-like-ads` +## 10. `usability.banner-blindness-dont-style-content-like-ads` Keep essential content and the primary action in the main column, styled like content, never in a right rail, top strip or animated coloured box. @@ -106,7 +120,7 @@ Keep essential content and the primary action in the main column, styled like co - **Look at:** Is the journey's primary action or a required notice positioned in the right rail (x > 70% of desktop viewport) or in a full-width top strip with background fill and animation? - **Unless:** On mobile, large inline ads do get fixated — the effect is weaker for inline placement -## 10. `usability.aesthetic-usability-effect-bias` +## 11. `usability.aesthetic-usability-effect-bias` Polish makes a screen look more usable than it is; discount your aesthetic impression and run the measurable checks first. @@ -117,7 +131,7 @@ Polish makes a screen look more usable than it is; discount your aesthetic impre - **Look at:** Did the evaluator judge the screen usable because it looks good? Re-check the verdict against the count-kind entries before trusting it; 'attractive' is the variable and stays taste. - **Unless:** With severe usability issues, or functionality sacrificed for aesthetics, users lose patience -## 11. `usability.natural-mapping-and-proximity` +## 12. `usability.natural-mapping-and-proximity` Put a control next to the thing it changes and a label next to its field; gaps between groups must exceed gaps within them. @@ -128,17 +142,6 @@ Put a control next to the thing it changes and a label next to its field; gaps b - **Look at:** Distance from each label to its own input versus the nearest other input; gap within a fieldset versus gap between fieldsets; distance from last input to the submit button. - **Unless:** Left-aligned labels are acceptable if space is constrained and labels are of similar length -## 12. `usability.mindless-clicks-not-fewer-clicks` - -Do not count clicks; make each one an unambiguous choice with clear link text that says where it goes. - -- **Source:** Steve Krug, *Don't Make Me Think, Revisited — chapter 4 (Krug's Second Law of Usability)* — https://ptgmedia.pearsoncmg.com/images/9780321965516/samplepages/0321965515.pdf -- **In their words:** "It doesn't matter how many times I have to click, as long as each click is a mindless, unambiguous choice." -- **Do:** Write link text that identifies its target; Use breadcrumbs and hub pages for scent; Keep clear labelling with strong information scent -- **Don't:** Count clicks as the metric; Use generic links like 'Click here' or 'Learn more' -- **Look at:** Count links and buttons whose text is generic ('Click here', 'Learn more', 'Read more', bare 'Next') or duplicates another link's text with a different target; do not measure path length. -- **Unless:** Fewer clicks matter more when the same path is drilled repeatedly or pages take long to load (Krug) - ## 13. `usability.start-with-user-needs-design-with-data` Name the actor and their need before drawing a screen, then let measured behaviour, not hunches or looks, decide what changes. @@ -185,7 +188,19 @@ To emphasize, quiet the competitors as much as you loudify the hero; never stack - **Unless:** The page title may be all-out up-pop - **Also stated as:** canon.tufte-smallest-effective-difference (Edward Tufte). -## 17. `craft.tap-targets-and-control-height` +## 17. `craft.wcag-contrast-and-dont-rely-on-colour-alone` + +Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. + +- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html +- **In their words:** "The WCAG recommended contrast ratio for body text — 4.5:1 to meet AA standards" +- **Do:** Style coloured badges as a soft background with dark text; Pair every colour state with an icon or label +- **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only +- **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. +- **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself +- **Also stated as:** modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group). + +## 18. `craft.tap-targets-and-control-height` Touch targets are at least 44×44; inputs and the buttons beside them share one height of 40 or 48px. @@ -197,7 +212,7 @@ Touch targets are at least 44×44; inputs and the buttons beside them share one - **Unless:** Inline text links in running prose; Dense desktop-only tools if the project commits to no touch - **Also stated as:** modern.hit-targets-meet-platform-minimums (Apple). -## 18. `craft.start-with-too-much-whitespace` +## 19. `craft.start-with-too-much-whitespace` Start with far more breathing room than feels necessary and remove it until it works; browser-default spacing is the largest mistake. @@ -208,7 +223,7 @@ Start with far more breathing room than feels necessary and remove it until it w - **Look at:** Ratio of vertical padding to font-size on list items, nav items and buttons; ratio of gap between groups to gap within a group. - **Unless:** Dense professional, interaction-heavy tools may compress, but spacing must stay consistent -## 19. `craft.type-scale-few-font-sizes` +## 20. `craft.type-scale-few-font-sizes` Use about four font sizes from a fixed scale; reuse the default size for body, menus, lists and controls. @@ -220,7 +235,7 @@ Use about four font sizes from a fixed scale; reuse the default size for body, m - **Unless:** Sizes must stay distinguishable, so large text may step many points apart; Marketing pages may add a display size - **Also stated as:** canon.bringhurst-compose-with-a-scale (Robert Bringhurst). -## 20. `craft.button-hierarchy-one-primary` +## 21. `craft.button-hierarchy-one-primary` One filled brand-colour button per view; secondaries outlined, tertiaries as text, destructive actions quiet with a confirmation step. @@ -231,7 +246,7 @@ One filled brand-colour button per view; secondaries outlined, tertiaries as tex - **Look at:** Classify buttons as filled, outlined or text from computed style; fail if a view or dialog has more than one filled button of distinct hues, or a delete-labelled button is filled while the confirming action is not. - **Unless:** Segmented or toggle groups; Toolbars of equal-weight actions; A page whose only job is the destructive action, where red is primary -## 21. `craft.text-on-images-needs-consistent-contrast` +## 22. `craft.text-on-images-needs-consistent-contrast` Put an overlay, scrim, box, blur or floor fade between photo and text, and test legibility at every viewport. @@ -243,18 +258,6 @@ Put an overlay, scrim, box, blur or floor fade between photo and text, and test - **Unless:** Decorative text that is not read, such as watermarks; A fixed, dark, low-contrast image - **Also stated as:** modern.glass-and-blur-earn-their-place (Megan Brown (NN/G)). -## 22. `craft.wcag-contrast-and-dont-rely-on-colour-alone` - -Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. - -- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html -- **In their words:** "The WCAG recommended contrast ratio for body text — 4.5:1 to meet AA standards" -- **Do:** Style coloured badges as a soft background with dark text; Pair every colour state with an icon or label -- **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only -- **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. -- **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself -- **Also stated as:** modern.contrast-and-not-colour-alone (Apple). - ## 23. `craft.greys-dont-have-to-be-grey-never-use-black` Tint the neutral scale toward the brand hue and never use pure black for text. @@ -322,18 +325,7 @@ Let something cross the fold or the container edge so the eye knows more continu - **Look at:** At each named viewport: an element crossing the viewport bottom edge; for horizontal scrollers, the last visible item extending past the container edge by a lens-set number of px. - **Unless:** Works less well when viewport sizes are unpredictable; Icons still need labels and testing -## 29. `modern.cards-are-a-choice-not-a-default` - -Cards are for browsing; use lists for searching and tables for comparing, and keep each card readable with one primary action. - -- **Source:** Stan Kirilov, *UI Card Design (StanVision journal)* — https://www.stan.vision/journal/ui-card-design-examples-best-practices-and-common-patterns (secondary) -- **In their words:** "Cards for browsing. Lists for searching. Tables for comparing." -- **Do:** Lists for ranked or homogeneous data, tables for comparison; Consistent card heights; The title as the link, with a pseudo-element extending the hit area -- **Don't:** Card grids for search results and settings; Whole-card anchor wrappers; More than 3 clickable elements per card; Nested cards -- **Look at:** Detect card grids (3+ siblings with equal border, shadow and radius); fail on text under 14px, over 3 interactive descendants per card, row heights differing over 20%, or an anchor wrapping more than heading plus paragraph. -- **Unless:** Masonry where variable height is the design choice; Media browsing such as Netflix or Pinterest - -## 30. `modern.no-junk-drawer-menus-or-unlabeled-icons` +## 29. `modern.no-junk-drawer-menus-or-unlabeled-icons` Icons carry labels, menus are named for what they hold, and no primary feature hides under More or an ellipsis. @@ -343,9 +335,9 @@ Icons carry labels, menus are named for what they hold, and no primary feature h - **Don't:** More, ellipsis or Tools catch-alls holding primary features; Icon-only toolbars without labels; Tooltips on disabled buttons - **Look at:** Icon-only buttons with no text and no aria-label fail; menus triggered by More, …, Tools or Options holding over 5 items or a primary-action label warn; disabled buttons with title or aria-describedby fail. - **Unless:** Universally recognised icons in tight toolbars, though even the hamburger is weaker than designers think -- **Also stated as:** usability.dont-make-me-think (Steve Krug). +- **Also stated as:** writing.buttons-name-the-action (IBM Carbon Design System); usability.dont-make-me-think (Steve Krug). -## 31. `modern.interactions-feel-immediate-under-200ms` +## 30. `modern.interactions-feel-immediate-under-200ms` Interaction transitions run 200ms or less with an ease-out curve so the interface feels immediate. @@ -356,7 +348,7 @@ Interaction transitions run 200ms or less with an ease-out curve so the interfac - **Look at:** Computed transition-duration and animation-duration on elements that change on :hover, :focus, [aria-expanded] or [data-state]; flag any over 300ms, warn over 200ms; ease-in on an entering element fails. - **Unless:** Toasts may run slower with a plain ease on purpose for tone; Large page or scene transitions and decorative loops sit outside the interaction budget; Exit animations can be a bit more relaxed -## 32. `modern.motion-values-proportional-to-trigger` +## 31. `modern.motion-values-proportional-to-trigger` Scale and fade motion starts near its resting size, in proportion to the trigger, never from zero or a heavy squash. @@ -367,7 +359,7 @@ Scale and fade motion starts near its resting size, in proportion to the trigger - **Look at:** Parse @keyframes and WAAPI keyframes on dialogs, popovers and buttons and read the starting scale(); :active transforms below about 0.9 fail. - **Unless:** Elements that genuinely originate from a point, such as a FAB expanding into a sheet, can grow from small -## 33. `modern.animate-only-transform-and-opacity` +## 32. `modern.animate-only-transform-and-opacity` Animate only transform and opacity, listing properties explicitly; never transition all or animate layout properties. @@ -378,7 +370,7 @@ Animate only transform and opacity, listing properties explicitly; never transit - **Look at:** Scan stylesheets and computed transition-property for all and for layout properties; scan @keyframes for width, height, top and left. - **Unless:** Accordion height animation via grid-template-rows or interpolate-size is layout by nature and accepted when it is the only honest way -## 34. `modern.motion-has-an-origin` +## 33. `modern.motion-has-an-origin` A popover or menu animates from the side facing its trigger; set transform-origin where the motion physically starts. @@ -389,7 +381,7 @@ A popover or menu animates from the side facing its trigger; set transform-origi - **Look at:** For each open popover or menu with a known trigger (aria-controls or aria-haspopup), compare the computed transform-origin with the side facing the trigger. - **Unless:** Centred modals have no anchor; a centred origin is right there -## 35. `modern.honour-prefers-reduced-motion` +## 34. `modern.honour-prefers-reduced-motion` Every large motion has a reduced variant under prefers-reduced-motion; fade instead of slide rather than ignoring the setting. @@ -400,7 +392,7 @@ Every large motion has a reduced variant under prefers-reduced-motion; fade inst - **Look at:** Emulate prefers-reduced-motion: reduce and list elements whose computed animation-name or transition-property still includes translations over about 20px or durations above zero. - **Unless:** Motion that is the content, such as a video or a chart drawing, is out of scope -## 36. `modern.mobile-inputs-do-not-zoom-or-trap` +## 35. `modern.mobile-inputs-do-not-zoom-or-trap` Inputs are at least 16px on phones, never autofocus on touch, and the page never disables zoom or blocks paste. @@ -411,7 +403,7 @@ Inputs are at least 16px on phones, never autofocus on touch, and the page never - **Look at:** Computed font-size of input, select and textarea at a 390px viewport under 16px; viewport meta containing user-scalable=no; paste handlers that preventDefault; [autofocus] present at phone width. - **Unless:** Desktop-only admin tools; Autofocus on desktop with a single primary input -## 37. `modern.focus-is-visible-and-unobscured` +## 36. `modern.focus-is-visible-and-unobscured` Every focusable element shows a visible focus ring on :focus-visible, and no sticky or fixed element ever covers it. @@ -421,7 +413,7 @@ Every focusable element shows a visible focus ring on :focus-visible, and no sti - **Don't:** outline: none or 0 with no replacement; Focus rings hidden under sticky headers - **Look at:** Tab through every focusable element; diff the focused and unfocused rect or read computed outline and box-shadow under :focus-visible; check the focused rect is not intersected by position fixed or sticky elements above it. -## 38. `modern.numbers-and-text-do-not-shift-layout` +## 37. `modern.numbers-and-text-do-not-shift-layout` Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. @@ -432,7 +424,7 @@ Numbers in columns and timers use tabular figures, images carry dimensions, and - **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. - **Unless:** Prose numbers -## 39. `modern.shadows-share-one-light-source` +## 38. `modern.shadows-share-one-light-source` All shadows on a page share one light direction and offset ratio, layered and tinted toward the background hue. @@ -443,7 +435,7 @@ All shadows on a page share one light direction and offset ratio, layered and ti - **Look at:** Collect every computed box-shadow; fail when offset signs mix across the page or distinct shadow strings exceed about 6; warn when shadow hue is far from the background hue with alpha above 0.5. - **Unless:** Inset shadows for sunken fields; Glows meant as glows -## 40. `modern.defaults-are-decisions-you-inherited` +## 39. `modern.defaults-are-decisions-you-inherited` A library or AI default is someone else's decision; commit to one written aesthetic direction instead of inheriting it. @@ -453,3 +445,70 @@ A library or AI default is someone else's decision; commit to one written aesthe - **Don't:** Untouched library defaults; Clean and modern as a brief; The AI-look constellation of purple gradient, Inter, identical cards, glass glow and bounce hover - **Look at:** Score the constellation: purple-to-blue/cyan gradients warn, Inter or Roboto with no display face notes, 3+ identical icon+h3+p cards note, backdrop-filter plus glow notes, gradient text on numerals warns, overshoot cubic-bezier on hover warns. - **Unless:** A purple brand is allowed to be purple; the tell is the constellation and the absence of a decision, not any one colour + +## 40. `color.from-tokens-not-hex` + +Every colour on the page comes from the design system's named tokens or palette functions, never from hex values copied into components. + +- **Source:** GOV.UK Design System (Government Digital Service), *Styles: Colour* — https://design-system.service.gov.uk/styles/colour/ +- **In their words:** "Do not copy the specific hexadecimal (hex) colour values." +- **Do:** Reference colour by role token (brand, text, error, border) rather than by value; Use palette colours (tints and shades of a few families) for supporting elements; Use a functional token only in the context it is designed for +- **Don't:** Hex literals in component styles; Near-duplicate colours (#1d70b8 next to #1d70b9) created by eye-dropping; Using the error token as a general red +- **Look at:** Collect every computed color, background-color, border-*-color, outline-color and fill/stroke of painted elements, and every value of CSS custom properties declared on :root (and on any theme selector). Count distinct painted colours that match no custom-property value (exact RGBA after resolution), and count pairs of painted colours closer than ΔE 2 that are not identical. +- **Unless:** Images, illustrations and embedded third-party widgets; Browser defaults on unstyled native controls; GOV.UK: palette colours (not functional ones) are allowed for illustrations and custom components + +## 41. `color.one-action-colour-apart-from-status` + +Links and primary actions share one interactive colour family, used consistently, and that colour is not the colour of errors, warnings or success. + +- **Source:** IBM Carbon Design System, *Elements: Color, Overview (Color anatomy)* — https://carbondesignsystem.com/elements/color/overview/ +- **In their words:** "The core blue family serves as the primary action color across all IBM products and experiences. Additional colors are used sparingly and purposefully." +- **Do:** One link colour, used only for links; Primary buttons in the brand or action colour; Danger colour reserved for destructive buttons, not for the primary action +- **Don't:** Links in the error red; Primary buttons in several different hues across screens; Static text coloured like links +- **Look at:** Take the computed text colour of every a[href] in running text and the background of every primary (first, filled, or type=submit) button. Count distinct hues among them (30° bins), and count links or primary buttons whose colour is within ΔE 10 of the page's error, warning or success colour. Also count non-interactive text elements painted in the link colour. +- **Unless:** Primer (GitHub) deliberately uses the success role for primary buttons; a system that states such a mapping consistently is following its own rule; Destructive primary actions (Delete account) take the danger colour on purpose; Navigation menus, where position signals the link + +## 42. `color.focus-ring-contrasts-with-its-surroundings` + +The focus indicator contrasts at least 3:1 with whatever it is drawn against: the page background for an outer ring, the component's own colours for an inner one. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast, Relationship with Focus Visible (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." +- **Do:** An outer ring that contrasts with the page background; A two-colour ring (dark and light) that holds on any background; A thick indicator rather than a 1px one +- **Don't:** A yellow outer ring on a white page; A focus border that changes hue inside the component without contrasting with its fill; Focus shown only by a background tint change +- **Look at:** Tab to each focusable control; diff the focused and unfocused screenshots of the control's box padded by a few pixels; for the changed pixels, take their colour and the colour of the unchanged pixels adjacent to them (page background outside, component fill inside). Count controls where no changed region reaches 3:1 against its adjacent colour. page.focus-visible checks only that some pixel changes; this checks that the change can be seen. +- **Unless:** Unmodified browser default focus styles; WCAG does not compare focused and unfocused states with each other; a background-only change is out of scope for 1.4.11 but fails Use of Color + +## 43. `color.few-families-in-proportion` + +Use a few colour families in a deliberate proportion (neutral base dominant, then primary, secondary and a small accent), not an even spread of many hues. + +- **Source:** U.S. Web Design System (GSA), *Design tokens: Theme color tokens* — https://designsystem.digital.gov/design-tokens/color/theme-tokens/ +- **In their words:** "about 60% of your site’s color would be the primary color family, about 30% would be the secondary color family, and about 10% would be the accent color families" +- **Do:** Neutral base for text and most surfaces; One primary family carrying most of the colour, one secondary, a small accent; Additional colours used sparingly and for a purpose (Carbon); Start in black and white, then add colour to support the message (USWDS) +- **Don't:** Five or more saturated hue families at similar weight on one screen; An accent that covers more area than the primary; A new hue introduced for a single component +- **Look at:** Screenshot the page and bucket every non-neutral pixel (HSL saturation above about 15%) by hue into 30° bins, ignoring images and status colours. How many hue families take more than 2% of the coloured area, and does the largest one carry most of it while the smallest (accent) stays near a tenth? +- **Unless:** USWDS: the proportions are for non-base colours; neutral text will usually dominate; Illustration, photography and data visualisation, which need their own palettes; Brands whose identity is multi-hue + +## 44. `data-display.no-tables-for-layout` + +A table is for comparing data in rows and columns, never for arranging content on the page; layout belongs to the grid. + +- **Source:** GOV.UK Design System (Government Digital Service), *Table — When not to use this component* — https://design-system.service.gov.uk/components/table/ +- **In their words:** "Never use the table component to layout content on a page." +- **Do:** CSS grid or flex for page and dashboard layout; A list, cards or a summary list for items that do not share columns; Tables only where every row has the same fields +- **Don't:** A table that positions a sidebar, form or dashboard tiles; Table cells holding headings, paragraphs or whole forms; role=presentation on a table that actually holds tabular data +- **Look at:** Count table elements that have no th and either contain headings, more than one paragraph per cell, form fieldsets or nested tables, or have a single row whose cells hold unrelated content blocks. +- **Unless:** HTML email, where tables are still the only reliable layout tool +- **Also stated as:** modern.cards-are-a-choice-not-a-default (Stan Kirilov). + +## 45. `forms.autocomplete-names-the-purpose` + +Every field that asks about the user — name, email, phone, address, postcode, birthday, card — carries the matching autocomplete token so browsers and assistive tech can fill and label it. + +- **Source:** W3C Web Accessibility Initiative, *Understanding WCAG 2.2 Success Criterion 1.3.5: Identify Input Purpose* — https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html +- **In their words:** "Use code to indicate the purpose of common inputs, where technology allows." +- **Do:** autocomplete='name' or 'given-name' and 'family-name' on name fields; 'email', 'tel', 'postal-code', 'street-address' or 'address-line1', 'bday-day'/'bday-month'/'bday-year'; 'shipping' and 'billing' section tokens on order forms; autocomplete='off' on the form, if needed, while each field still declares its purpose +- **Don't:** autocomplete='off' on personal fields with no purpose token; Personal-data fields with no autocomplete attribute; A token that does not match the field (email token on a phone field) +- **Look at:** For every input, select and textarea whose label matches name, email, phone, address, postcode/ZIP, city, country, date of birth, card number or expiry: count those whose autocomplete attribute is missing, 'off', or not a WCAG input-purpose token that matches the label. +- **Unless:** Fields about someone other than the user (a recipient's email) are outside 1.3.5; A field that accepts either username or email may carry one token or none diff --git a/.claude/skills/uxcli/lenses/shop.md b/.claude/skills/uxcli/lenses/shop.md index 6453b6e..1f8d753 100644 --- a/.claude/skills/uxcli/lenses/shop.md +++ b/.claude/skills/uxcli/lenses/shop.md @@ -2,9 +2,33 @@ Catalogues, search results and product pages: browse, compare, choose. -43 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. +60 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. -## 1. `usability.signifiers-make-clickable-look-clickable` +## 1. `usability.visibility-of-system-status` + +Every action with consequences shows the user something changed, as quickly as possible, so they always know what the system is doing. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #1* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "The design should always keep users informed about what is going on, through appropriate feedback within a reasonable amount of time." +- **Do:** Show a visible trace of every state change; Give feedback immediately, or as quickly as possible; Disable and label a control while its action runs +- **Don't:** Submit silently; Change state with no visible trace; Show an action's result only somewhere else +- **Look at:** Drive an action, then diff DOM or pixels over the next frames: did anything visibly change within 1 s; is there a live region, status text, spinner or busy state? +- **Unless:** Below 0.1 s no special feedback is needed beyond showing the result +- **Also stated as:** modern.feedback-is-local-and-optimistic (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn); writing.success-names-what-happened (Shopify Polaris); data-display.loading-skeleton-holds-the-layout (GitHub Primer); forms.do-not-disable-the-submit-button (GitHub Primer). + +## 2. `usability.speak-the-users-language` + +Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #2* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "The design should speak the users' language. Use words, phrases, and concepts familiar to the user, rather than internal jargon." +- **Do:** Use words the user understands without looking them up; Spell acronyms out in full on each page; Write 'for example', not 'eg' or 'ie' +- **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations +- **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. +- **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se +- **Also stated as:** writing.errors-say-what-and-how-to-fix (GOV.UK Design System (Government Digital Service)); writing.plain-language-reading-level (Shopify Polaris); forms.error-message-says-how-to-fix (GOV.UK Design System); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen). + +## 3. `usability.signifiers-make-clickable-look-clickable` Links and buttons must look clickable — colour, underline, border or fill — because weak signifiers cost measured time and fixations. @@ -14,9 +38,21 @@ Links and buttons must look clickable — colour, underline, border or fill — - **Don't:** Style linked text as static text; Use ghost buttons as the default; Use disabled buttons if avoidable; Rely on a label to say that a control is a control - **Look at:** For each a[href], button, [role=button]: computed colour, underline, border and background versus surrounding text; a link matching body colour with no underline and no other differentiator is weak; count disabled buttons. - **Unless:** Link position in nav menus or peripheral lists may eliminate the need for underlining (Loranger); Flat works best with low information density, traditional layouts and high-contrast targets positioned standardly (Moran) -- **Also stated as:** modern.signifiers-survive-flatness (Kate Moran (NN/G)); canon.gestalt-similarity (Aurora Harley, NN/g); modern.no-dead-zones-and-honest-clickability (Rauno Freiberg). +- **Also stated as:** usability.banner-blindness-dont-style-content-like-ads (Kara Pernice, NN/g); craft.button-hierarchy-one-primary (Steve Schoger); canon.gestalt-similarity (Aurora Harley, NN/g); color.one-action-colour-apart-from-status (IBM Carbon Design System). + +## 4. `usability.feedback-within-a-second` + +Paint something within 0.1 s; keep the user's flow with a response under 1 s; show a progress indicator for anything longer. + +- **Source:** Jakob Nielsen, *Response Times: The 3 Important Limits (1993)* — https://www.nngroup.com/articles/response-times-3-important-limits/ (study) +- **In their words:** "0.1 second is about the limit for having the user feel that the system is reacting instantaneously … 1.0 second is about the limit for the user's flow of thought to stay uninterrupted" +- **Do:** Use a progress indicator for any action over about 1 s; Use a looped indicator for 2–9 s waits; Use a percent-done indicator for 10 s or more +- **Don't:** Leave dead time with no indicator; Show a percent-done bar that lies badly +- **Look at:** Time from input event to first paint of any change; presence of a progress element, aria-busy, or role=status update when the wait exceeds 1 s. +- **Unless:** Laws of UX claims a purposeful delay can raise perceived value — an opinion with no study cited +- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg). -## 2. `usability.fitts-target-size-and-distance` +## 5. `usability.fitts-target-size-and-distance` Make targets big, space them apart, and put them where the pointer already is; touch targets at least 1 cm square. @@ -28,7 +64,7 @@ Make targets big, space them apart, and put them where the pointer already is; t - **Unless:** Infinite targets along screen edges for mouse — size matters less at an edge; Primary CTAs, moving users, children and the elderly need larger than the minimum - **Also stated as:** craft.tap-targets-and-control-height (Erik D. Kennedy); craft.growth-design-psychology-principles (Growth.Design (Dan Benoni, Louis-Xavier Lavallée)); modern.hit-targets-meet-platform-minimums (Apple). -## 3. `usability.von-restorff-one-emphasis` +## 6. `usability.von-restorff-one-emphasis` One visually distinct primary action per view, left-aligned with the form, and distinguished by more than colour. @@ -38,33 +74,20 @@ One visually distinct primary action per view, left-aligned with the form, and d - **Don't:** Put two or more filled primary buttons in one view; Signal emphasis by colour alone - **Look at:** Cluster interactive elements by computed background, border and weight; the most emphatic cluster per view must have one member; primary's left edge aligns with the inputs; emphasis differs in weight, fill or border, not colour only. - **Unless:** Do not pre-highlight a dangerous action as the default (Nielsen) -- **Also stated as:** craft.button-hierarchy-one-primary (Steve Schoger); canon.vignelli-weight-for-function-not-volume (Massimo Vignelli). - -## 4. `usability.feedback-within-a-second` +- **Also stated as:** canon.vignelli-weight-for-function-not-volume (Massimo Vignelli). -Paint something within 0.1 s; keep the user's flow with a response under 1 s; show a progress indicator for anything longer. +## 7. `usability.no-false-affordances` -- **Source:** Jakob Nielsen, *Response Times: The 3 Important Limits (1993)* — https://www.nngroup.com/articles/response-times-3-important-limits/ (study) -- **In their words:** "0.1 second is about the limit for having the user feel that the system is reacting instantaneously … 1.0 second is about the limit for the user's flow of thought to stay uninterrupted" -- **Do:** Use a progress indicator for any action over about 1 s; Use a looped indicator for 2–9 s waits; Use a percent-done indicator for 10 s or more -- **Don't:** Leave dead time with no indicator; Show a percent-done bar that lies badly -- **Look at:** Time from input event to first paint of any change; presence of a progress element, aria-busy, or role=status update when the wait exceeds 1 s. -- **Unless:** Laws of UX claims a purposeful delay can raise perceived value — an opinion with no study cited -- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn). - -## 5. `usability.visibility-of-system-status` - -Every action with consequences shows the user something changed, as quickly as possible, so they always know what the system is doing. +Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #1* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "The design should always keep users informed about what is going on, through appropriate feedback within a reasonable amount of time." -- **Do:** Show a visible trace of every state change; Give feedback immediately, or as quickly as possible; Disable and label a control while its action runs -- **Don't:** Submit silently; Change state with no visible trace; Show an action's result only somewhere else -- **Look at:** Drive an action, then diff DOM or pixels over the next frames: did anything visibly change within 1 s; is there a live region, status text, spinner or busy state? -- **Unless:** Below 0.1 s no special feedback is needed beyond showing the result -- **Also stated as:** modern.feedback-is-local-and-optimistic (Rauno Freiberg). +- **Source:** Hoa Loranger, NN/g, *Beyond Blue Links: Making Clickable Elements Recognizable (2015)* — https://www.nngroup.com/articles/clickable-elements/ +- **In their words:** "Avoid making non-clickable items (like headings) resemble buttons." +- **Do:** Reserve link colour and underline for links; Reserve button shape and fill for buttons +- **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons +- **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). +- **Also stated as:** modern.no-dead-zones-and-honest-clickability (Rauno Freiberg). -## 6. `usability.follow-conventions` +## 8. `usability.follow-conventions` Work the way the sites and platforms users already know; a convention beats a locally optimised novelty. @@ -76,7 +99,7 @@ Work the way the sites and platforms users already know; a convention beats a lo - **Unless:** Desktop apps follow their own OS: Windows OK-first, Apple OK-last - **Also stated as:** craft.anything-but-dropdowns (Erik D. Kennedy); modern.boring-and-familiar-beats-novel (Scott Berkun). -## 7. `usability.visual-hierarchy-for-scanning` +## 9. `usability.visual-hierarchy-for-scanning` Design for scanning, not reading: headings that out-rank body text, bold key phrases, lists, and no walls of unformatted text. @@ -88,7 +111,7 @@ Design for scanning, not reading: headings that out-rank body text, bold key phr - **Unless:** The pattern needs moderate, not high, interest — highly motivated readers read - **Also stated as:** craft.hierarchy-is-everything-squint-test (Erik D. Kennedy). -## 8. `usability.hicks-fewer-choices-when-time-matters` +## 10. `usability.hicks-fewer-choices-when-time-matters` Fewer equal-weight choices at a decision point, with one recommended; but never use seven as a cap on menu length. @@ -99,37 +122,17 @@ Fewer equal-weight choices at a decision point, with one recommended; but never - **Look at:** Count same-weight primary actions in a view; count options in a select or radio group with no recommended default. A menu's item count alone is not a failure. - **Unless:** Recognition, not recall, governs menus — length is a scan cost, not a memory cost (Nielsen) -## 9. `usability.banner-blindness-dont-style-content-like-ads` - -Keep essential content and the primary action in the main column, styled like content, never in a right rail, top strip or animated coloured box. - -- **Source:** Kara Pernice, NN/g, *Banner Blindness Revisited (2018)* — https://www.nngroup.com/articles/banner-blindness-old-and-new-findings/ (study) -- **In their words:** "Users have learned to ignore content that resembles ads, is close to ads, or appears in locations traditionally dedicated to ads." -- **Do:** Put essential content in the main column; Style key notices like content -- **Don't:** Put the primary CTA in the right rail or a top banner strip; Give a key notice animation, coloured background or fancy formatting; Place essential content next to real ads -- **Look at:** Is the journey's primary action or a required notice positioned in the right rail (x > 70% of desktop viewport) or in a full-width top strip with background fill and animation? -- **Unless:** On mobile, large inline ads do get fixated — the effect is weaker for inline placement - -## 10. `usability.speak-the-users-language` - -Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. +## 11. `usability.mindless-clicks-not-fewer-clicks` -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #2* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "The design should speak the users' language. Use words, phrases, and concepts familiar to the user, rather than internal jargon." -- **Do:** Use words the user understands without looking them up; Spell acronyms out in full on each page; Write 'for example', not 'eg' or 'ie' -- **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations -- **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. -- **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se - -## 11. `usability.no-false-affordances` - -Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. +Do not count clicks; make each one an unambiguous choice with clear link text that says where it goes. -- **Source:** Hoa Loranger, NN/g, *Beyond Blue Links: Making Clickable Elements Recognizable (2015)* — https://www.nngroup.com/articles/clickable-elements/ -- **In their words:** "Avoid making non-clickable items (like headings) resemble buttons." -- **Do:** Reserve link colour and underline for links; Reserve button shape and fill for buttons -- **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons -- **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). +- **Source:** Steve Krug, *Don't Make Me Think, Revisited — chapter 4 (Krug's Second Law of Usability)* — https://ptgmedia.pearsoncmg.com/images/9780321965516/samplepages/0321965515.pdf +- **In their words:** "It doesn't matter how many times I have to click, as long as each click is a mindless, unambiguous choice." +- **Do:** Write link text that identifies its target; Use breadcrumbs and hub pages for scent; Keep clear labelling with strong information scent +- **Don't:** Count clicks as the metric; Use generic links like 'Click here' or 'Learn more' +- **Look at:** Count links and buttons whose text is generic ('Click here', 'Learn more', 'Read more', bare 'Next') or duplicates another link's text with a different target; do not measure path length. +- **Unless:** Fewer clicks matter more when the same path is drilled repeatedly or pages take long to load (Krug) +- **Also stated as:** writing.links-describe-their-destination (Mailchimp). ## 12. `usability.aesthetic-usability-effect-bias` @@ -164,18 +167,7 @@ Put a control next to the thing it changes and a label next to its field; gaps b - **Look at:** Distance from each label to its own input versus the nearest other input; gap within a fieldset versus gap between fieldsets; distance from last input to the submit button. - **Unless:** Left-aligned labels are acceptable if space is constrained and labels are of similar length -## 15. `usability.mindless-clicks-not-fewer-clicks` - -Do not count clicks; make each one an unambiguous choice with clear link text that says where it goes. - -- **Source:** Steve Krug, *Don't Make Me Think, Revisited — chapter 4 (Krug's Second Law of Usability)* — https://ptgmedia.pearsoncmg.com/images/9780321965516/samplepages/0321965515.pdf -- **In their words:** "It doesn't matter how many times I have to click, as long as each click is a mindless, unambiguous choice." -- **Do:** Write link text that identifies its target; Use breadcrumbs and hub pages for scent; Keep clear labelling with strong information scent -- **Don't:** Count clicks as the metric; Use generic links like 'Click here' or 'Learn more' -- **Look at:** Count links and buttons whose text is generic ('Click here', 'Learn more', 'Read more', bare 'Next') or duplicates another link's text with a different target; do not measure path length. -- **Unless:** Fewer clicks matter more when the same path is drilled repeatedly or pages take long to load (Krug) - -## 16. `usability.start-with-user-needs-design-with-data` +## 15. `usability.start-with-user-needs-design-with-data` Name the actor and their need before drawing a screen, then let measured behaviour, not hunches or looks, decide what changes. @@ -185,7 +177,7 @@ Name the actor and their need before drawing a screen, then let measured behavio - **Don't:** Design to a hunch; Design to an aesthetic - **Look at:** Does the journey name its actor and need (uxcli context show), and is the design decision traceable to observed behaviour rather than a hunch or a look? -## 17. `craft.fewer-borders` +## 16. `craft.fewer-borders` Separate elements with space, a background shift, a shadow or striping before reaching for a border; too many borders make a design busy. @@ -195,9 +187,9 @@ Separate elements with space, a background shift, a shadow or striping before re - **Don't:** Use a 1px border as the default grouping device; Wrap a nav in a panel that only needs to recede; Rule every table row with a border when striping would do - **Look at:** For every text node, count ancestors with a visible border, non-none box-shadow or outline; report max nesting depth and bordered boxes per viewport. Counted by `page.nesting`: `review check` refuses a `holds` it contradicts. - **Unless:** Form inputs and the one object a person acts on keep their border; Keylines that make disconnected content feel connected; Decorative borders as style once the count is low -- **Also stated as:** canon.gestalt-common-region (Aurora Harley, NN/g); canon.rams-as-little-design-as-possible (Dieter Rams); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); canon.tufte-smallest-effective-difference (Edward Tufte); canon.tufte-one-plus-one-equals-three (Edward Tufte). +- **Also stated as:** canon.gestalt-common-region (Aurora Harley, NN/g); forms.group-related-inputs-in-a-fieldset (U.S. Web Design System (USWDS)); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); modern.cards-are-a-choice-not-a-default (Stan Kirilov); canon.tufte-one-plus-one-equals-three (Edward Tufte); craft.separation-order-space-then-lines-then-boxes (Adam Wathan & Steve Schoger; Erik D. Kennedy). -## 18. `craft.spacing-and-sizing-system` +## 17. `craft.spacing-and-sizing-system` Draw every margin, padding and gap from one scale, and make space between groups clearly larger than space within them. @@ -207,19 +199,9 @@ Draw every margin, padding and gap from one scale, and make space between groups - **Don't:** Use one-off values such as 13px or 27px; Use equal gaps across a group boundary - **Look at:** Collect all computed margin, padding and gap values above 0; report distinct values and how many are off a 4px grid; compare label-to-field gap with field-to-next-label gap. - **Unless:** Optical adjustments on icons and hanging punctuation sit a pixel or two off-scale -- **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen); modern.spacing-comes-from-a-scale (Stan Kirilov). - -## 19. `craft.separation-order-space-then-lines-then-boxes` - -Use the lightest separator that works: more space first, then a keyline or background band, and a box only for the object that is acted on. - -- **Source:** Adam Wathan & Steve Schoger; Erik D. Kennedy, *Refactoring UI (fewer-borders tactic); 7 Rules for Creating Gorgeous UI, Part 1* — https://www.refactoringui.com/ (folklore: the wording is not verified) -- **Do:** Separate with whitespace by default; Add a keyline or background band only when space alone fails; Box only the object a person acts on -- **Don't:** Reach for a card or border first -- **Look at:** Between sibling groups, record which separator is used: gap at least 2x the inner gap (space), hr or border-bottom (line), bordered or shadowed wrapper (box); report the inner/outer gap ratio. -- **Unless:** Dense data such as tables where zebra stripes or keylines are used; Interactive cards that are the unit of action +- **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.spacing-comes-from-a-scale (Stan Kirilov). -## 20. `craft.de-emphasize-to-emphasize-up-pop-down-pop` +## 18. `craft.de-emphasize-to-emphasize-up-pop-down-pop` To emphasize, quiet the competitors as much as you loudify the hero; never stack every up-pop property on non-title elements. @@ -229,8 +211,9 @@ To emphasize, quiet the competitors as much as you loudify the hero; never stack - **Don't:** Stack big, bold, bright and uppercase on any element that is not the page title - **Look at:** Flag text at 2x body size or more that is also font-weight 700+, full contrast and uppercase, when it is not the page h1. - **Unless:** The page title may be all-out up-pop +- **Also stated as:** canon.tufte-smallest-effective-difference (Edward Tufte). -## 21. `craft.wcag-contrast-and-dont-rely-on-colour-alone` +## 19. `craft.wcag-contrast-and-dont-rely-on-colour-alone` Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. @@ -240,9 +223,21 @@ Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dar - **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only - **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. - **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself -- **Also stated as:** modern.contrast-and-not-colour-alone (Apple). +- **Also stated as:** modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group). -## 22. `craft.greys-dont-have-to-be-grey-never-use-black` +## 20. `craft.black-and-white-first-limit-hues` + +Design in greyscale first, then add one accent hue with purpose; vary by saturation and brightness, not by more hues. + +- **Source:** Erik D. Kennedy, *7 Rules for Creating Gorgeous UI, Part 1* — https://www.learnui.design/blog/7-rules-for-creating-gorgeous-ui-part-1.html +- **In their words:** "Design black and white first … Add color last, and even then, only with purpose." +- **Do:** Use greyscale plus one or two accent hues; Vary colour by saturation and brightness within a hue +- **Don't:** Use complementary or triadic colour-theory palettes as UI palettes; Give each section its own hue +- **Look at:** Cluster computed colours with saturation above 25% by hue within 15 degrees; count clusters, excluding declared semantic states such as error, success and warning. +- **Unless:** Sporty, flashy or cartoony brands that need a colour-fluent designer; Data-visualisation palettes +- **Also stated as:** color.few-families-in-proportion (U.S. Web Design System (GSA)). + +## 21. `craft.greys-dont-have-to-be-grey-never-use-black` Tint the neutral scale toward the brand hue and never use pure black for text. @@ -253,7 +248,7 @@ Tint the neutral scale toward the brand hue and never use pure black for text. - **Look at:** Count text, border and background colours with saturation 0 and lightness under 15%, and the share of neutral swatches with saturation exactly 0. - **Unless:** It does not always work; worth a trial; High-contrast or accessibility modes and print -## 23. `craft.start-with-too-much-whitespace` +## 22. `craft.start-with-too-much-whitespace` Start with far more breathing room than feels necessary and remove it until it works; browser-default spacing is the largest mistake. @@ -264,7 +259,7 @@ Start with far more breathing room than feels necessary and remove it until it w - **Look at:** Ratio of vertical padding to font-size on list items, nav items and buttons; ratio of gap between groups to gap within a group. - **Unless:** Dense professional, interaction-heavy tools may compress, but spacing must stay consistent -## 24. `craft.type-scale-few-font-sizes` +## 23. `craft.type-scale-few-font-sizes` Use about four font sizes from a fixed scale; reuse the default size for body, menus, lists and controls. @@ -275,18 +270,18 @@ Use about four font sizes from a fixed scale; reuse the default size for body, m - **Look at:** Count distinct computed font-size values on visible text per viewport and how many fall outside the declared scale. - **Unless:** Sizes must stay distinguishable, so large text may step many points apart; Marketing pages may add a display size -## 25. `craft.black-and-white-first-limit-hues` +## 24. `canon.rams-as-little-design-as-possible` -Design in greyscale first, then add one accent hue with purpose; vary by saturation and brightness, not by more hues. +Remove everything that does not serve the purpose; less, but better. -- **Source:** Erik D. Kennedy, *7 Rules for Creating Gorgeous UI, Part 1* — https://www.learnui.design/blog/7-rules-for-creating-gorgeous-ui-part-1.html -- **In their words:** "Design black and white first … Add color last, and even then, only with purpose." -- **Do:** Use greyscale plus one or two accent hues; Vary colour by saturation and brightness within a hue -- **Don't:** Use complementary or triadic colour-theory palettes as UI palettes; Give each section its own hue -- **Look at:** Cluster computed colours with saturation above 25% by hue within 15 degrees; count clusters, excluding declared semantic states such as error, success and warning. -- **Unless:** Sporty, flashy or cartoony brands that need a colour-fluent designer; Data-visualisation palettes +- **Source:** Dieter Rams, *Ten principles for good design* — https://www.vitsoe.com/us/about/good-design +- **In their words:** "Good design is as little design as possible. Less, but better – because it concentrates on the essential aspects, and the products are not burdened with non-essentials." +- **Do:** Remove anything that does not serve the purpose; Keep one border where one will do +- **Don't:** Add ornament or decoration for its own sake; Repeat the heading as a decorative icon; Wrap content in extra bordered or shadowed layers +- **Look at:** Decorative-only elements (no text, image or interactive role, only border/shadow/background) and distinct box-shadow, border-radius and gradient declarations in computed styles. +- **Unless:** Aesthetic quality is part of usefulness (principle 3): stripping to nothing is not the goal -## 26. `canon.bringhurst-leading-is-a-rhythmic-unit` +## 25. `canon.bringhurst-leading-is-a-rhythmic-unit` The leading is the vertical unit; add and remove vertical space in multiples of it. @@ -297,7 +292,7 @@ The leading is the vertical unit; add and remove vertical space in multiples of - **Look at:** Body line-height L; margins and paddings between text blocks as multiples of L (or L/2 if the lens allows). - **Unless:** More leading for longer measures, darker faces, larger x-height and sans serifs: the ratio moves with the face -## 27. `canon.vignelli-grid-module-fits-the-job` +## 26. `canon.vignelli-grid-module-fits-the-job` Lay everything on one grid whose module is coarse enough to constrain and fine enough to serve the content. @@ -308,7 +303,7 @@ Lay everything on one grid whose module is coarse enough to constrain and fine e - **Look at:** Distinct left-edge x positions of top-level blocks and their fit to the best k-column model (residual within 2px). - **Unless:** Outside margins may be made deliberately small to give tension between page edge and content -## 28. `canon.bringhurst-letterspace-caps-5-10` +## 27. `canon.bringhurst-letterspace-caps-5-10` Letterspace all-caps and small-caps runs by 5–10% of the type size; do not letterspace lowercase. @@ -319,18 +314,41 @@ Letterspace all-caps and small-caps runs by 5–10% of the type size; do not let - **Look at:** For text with text-transform uppercase or all-caps content of ≥3 letters: computed letter-spacing divided by font-size; same ratio on lowercase paragraphs. - **Unless:** Display type at large sizes may be tracked tighter (not stated on the fetched page) -## 29. `modern.cards-are-a-choice-not-a-default` +## 28. `modern.every-state-is-designed` + +Empty, sparse, dense, error and loading states are designed; skeletons match final layout and long content never overflows. -Cards are for browsing; use lists for searching and tables for comparing, and keep each card readable with one primary action. +- **Source:** Vercel Labs, *Web Interface Guidelines — AGENTS.md* — https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/AGENTS.md +- **In their words:** "MUST: Design empty/sparse/dense/error states" +- **Do:** An empty state with a primary create action; Skeletons sized like real rows; min-w-0 and truncation on flex children +- **Don't:** Blank screens on empty arrays; Spinners that reflow content; Overflow from long strings +- **Look at:** Render with [], with 1 item, with 500 items and with a 300-character title; assert no horizontal overflow, no overlap, an actionable control in the empty state, and skeleton-to-loaded CLS under a threshold. +- **Also stated as:** writing.empty-states-say-what-next (GitHub Primer); data-display.empty-data-explains-and-offers-next-step (IBM Carbon Design System); data-display.wrap-before-truncating-and-reveal-the-rest (GitHub Primer). -- **Source:** Stan Kirilov, *UI Card Design (StanVision journal)* — https://www.stan.vision/journal/ui-card-design-examples-best-practices-and-common-patterns (secondary) -- **In their words:** "Cards for browsing. Lists for searching. Tables for comparing." -- **Do:** Lists for ranked or homogeneous data, tables for comparison; Consistent card heights; The title as the link, with a pseudo-element extending the hit area -- **Don't:** Card grids for search results and settings; Whole-card anchor wrappers; More than 3 clickable elements per card; Nested cards -- **Look at:** Detect card grids (3+ siblings with equal border, shadow and radius); fail on text under 14px, over 3 interactive descendants per card, row heights differing over 20%, or an anchor wrapping more than heading plus paragraph. -- **Unless:** Masonry where variable height is the design choice; Media browsing such as Netflix or Pinterest +## 29. `modern.no-junk-drawer-menus-or-unlabeled-icons` -## 30. `modern.frequent-actions-do-not-animate` +Icons carry labels, menus are named for what they hold, and no primary feature hides under More or an ellipsis. + +- **Source:** Jakob Nielsen (NN/G), *Top 10 Application-Design Mistakes* — https://www.nngroup.com/articles/top-10-application-design-mistakes/ +- **In their words:** "most icons, unless they have a text label next to them, will be difficult or impossible for users to understand" +- **Do:** Labelled icons; Menus named for their contents; Inline help before tooltips +- **Don't:** More, ellipsis or Tools catch-alls holding primary features; Icon-only toolbars without labels; Tooltips on disabled buttons +- **Look at:** Icon-only buttons with no text and no aria-label fail; menus triggered by More, …, Tools or Options holding over 5 items or a primary-action label warn; disabled buttons with title or aria-describedby fail. +- **Unless:** Universally recognised icons in tight toolbars, though even the hamburger is weaker than designers think +- **Also stated as:** writing.buttons-name-the-action (IBM Carbon Design System); usability.dont-make-me-think (Steve Krug). + +## 30. `modern.signifiers-survive-flatness` + +Flat styling keeps its signifiers: buttons have a fill or border, links are underlined or distinctly coloured, inputs show a boundary. + +- **Source:** Kate Moran (NN/G), *Flat UI Elements Attract Less Attention and Cause Uncertainty* — https://www.nngroup.com/articles/flat-ui-less-attention-cause-uncertainty/ (study) +- **In their words:** "participants spent 22% more time… looking at the pages with weak signifiers" +- **Do:** Buttons with a discernible fill or border; Links underlined or in a distinct colour; Inputs with a visible boundary +- **Don't:** Text-only buttons indistinguishable from labels; Links styled as body text; Long shadows and other purely aesthetic depth +- **Look at:** For each button or role=button: background differs from parent, or border width above 0, or text colour is the accent, else fail; an in-text link with no text-decoration and colour close to the surrounding text fails. +- **Unless:** Toolbar icon buttons with tooltips and hover fills; Navigation bars recognised by position + +## 31. `modern.frequent-actions-do-not-animate` Actions used many times a day, and anything keyboard-triggered, appear instantly without an enter animation. @@ -342,19 +360,31 @@ Actions used many times a day, and anything keyboard-triggered, appear instantly - **Unless:** macOS context menus fade out and blink the chosen item; Rare features may be theatrical (delight-impact curve) - **Also stated as:** modern.delight-scales-with-rarity (Benji Taylor). -## 31. `modern.no-junk-drawer-menus-or-unlabeled-icons` +## 32. `modern.mobile-inputs-do-not-zoom-or-trap` -Icons carry labels, menus are named for what they hold, and no primary feature hides under More or an ellipsis. +Inputs are at least 16px on phones, never autofocus on touch, and the page never disables zoom or blocks paste. -- **Source:** Jakob Nielsen (NN/G), *Top 10 Application-Design Mistakes* — https://www.nngroup.com/articles/top-10-application-design-mistakes/ -- **In their words:** "most icons, unless they have a text label next to them, will be difficult or impossible for users to understand" -- **Do:** Labelled icons; Menus named for their contents; Inline help before tooltips -- **Don't:** More, ellipsis or Tools catch-alls holding primary features; Icon-only toolbars without labels; Tooltips on disabled buttons -- **Look at:** Icon-only buttons with no text and no aria-label fail; menus triggered by More, …, Tools or Options holding over 5 items or a primary-action label warn; disabled buttons with title or aria-describedby fail. -- **Unless:** Universally recognised icons in tight toolbars, though even the hamburger is weaker than designers think -- **Also stated as:** usability.dont-make-me-think (Steve Krug). +- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ +- **In their words:** "Font size for inputs should not be smaller than 16px to prevent iOS zooming on focus" +- **Do:** Set type, inputmode and autocomplete on every input; Use touch-action: manipulation on controls +- **Don't:** Viewport meta with user-scalable=no; Paste handlers that preventDefault; autofocus at phone widths +- **Look at:** Computed font-size of input, select and textarea at a 390px viewport under 16px; viewport meta containing user-scalable=no; paste handlers that preventDefault; [autofocus] present at phone width. +- **Unless:** Desktop-only admin tools; Autofocus on desktop with a single primary input +- **Also stated as:** forms.input-type-matches-the-answer (GOV.UK Design System). -## 32. `modern.motion-values-proportional-to-trigger` +## 33. `modern.numbers-and-text-do-not-shift-layout` + +Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. + +- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ +- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" +- **Do:** tabular-nums in tables, timers and prices; width and height on every img +- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover +- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. +- **Unless:** Prose numbers +- **Also stated as:** data-display.right-align-numbers-tabular-figures (GitHub Primer). + +## 34. `modern.motion-values-proportional-to-trigger` Scale and fade motion starts near its resting size, in proportion to the trigger, never from zero or a heavy squash. @@ -365,7 +395,7 @@ Scale and fade motion starts near its resting size, in proportion to the trigger - **Look at:** Parse @keyframes and WAAPI keyframes on dialogs, popovers and buttons and read the starting scale(); :active transforms below about 0.9 fail. - **Unless:** Elements that genuinely originate from a point, such as a FAB expanding into a sheet, can grow from small -## 33. `modern.animate-only-transform-and-opacity` +## 35. `modern.animate-only-transform-and-opacity` Animate only transform and opacity, listing properties explicitly; never transition all or animate layout properties. @@ -376,7 +406,7 @@ Animate only transform and opacity, listing properties explicitly; never transit - **Look at:** Scan stylesheets and computed transition-property for all and for layout properties; scan @keyframes for width, height, top and left. - **Unless:** Accordion height animation via grid-template-rows or interpolate-size is layout by nature and accepted when it is the only honest way -## 34. `modern.motion-has-an-origin` +## 36. `modern.motion-has-an-origin` A popover or menu animates from the side facing its trigger; set transform-origin where the motion physically starts. @@ -387,7 +417,7 @@ A popover or menu animates from the side facing its trigger; set transform-origi - **Look at:** For each open popover or menu with a known trigger (aria-controls or aria-haspopup), compare the computed transform-origin with the side facing the trigger. - **Unless:** Centred modals have no anchor; a centred origin is right there -## 35. `modern.honour-prefers-reduced-motion` +## 37. `modern.honour-prefers-reduced-motion` Every large motion has a reduced variant under prefers-reduced-motion; fade instead of slide rather than ignoring the setting. @@ -398,7 +428,7 @@ Every large motion has a reduced variant under prefers-reduced-motion; fade inst - **Look at:** Emulate prefers-reduced-motion: reduce and list elements whose computed animation-name or transition-property still includes translations over about 20px or durations above zero. - **Unless:** Motion that is the content, such as a video or a chart drawing, is out of scope -## 36. `modern.animations-are-interruptible` +## 38. `modern.animations-are-interruptible` An open or close animation can be reversed mid-flight by the next input; nothing waits for a transition to finish. @@ -409,7 +439,7 @@ An open or close animation can be reversed mid-flight by the next input; nothing - **Look at:** Open a panel, immediately send the close input, sample the element's bounding box about 50ms later; if it is still growing the animation was not interruptible. Depends on timing tolerance. - **Unless:** Destructive commits that fire only on gesture end are about triggering, not interruptibility -## 37. `modern.readable-type-sizes-and-weights` +## 39. `modern.readable-type-sizes-and-weights` Body text sits at the platform default size, weights stay 400 or heavier, and weight never changes on hover. @@ -420,18 +450,7 @@ Body text sits at the platform default size, weights stay 400 or heavier, and we - **Look at:** Computed font-size and font-weight of every text node at a phone viewport; count distinct font-family stacks and warn above 2. - **Unless:** Captions and legal text may sit at the platform minimum; Display headings may use light weights at large sizes -## 38. `modern.mobile-inputs-do-not-zoom-or-trap` - -Inputs are at least 16px on phones, never autofocus on touch, and the page never disables zoom or blocks paste. - -- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ -- **In their words:** "Font size for inputs should not be smaller than 16px to prevent iOS zooming on focus" -- **Do:** Set type, inputmode and autocomplete on every input; Use touch-action: manipulation on controls -- **Don't:** Viewport meta with user-scalable=no; Paste handlers that preventDefault; autofocus at phone widths -- **Look at:** Computed font-size of input, select and textarea at a 390px viewport under 16px; viewport meta containing user-scalable=no; paste handlers that preventDefault; [autofocus] present at phone width. -- **Unless:** Desktop-only admin tools; Autofocus on desktop with a single primary input - -## 39. `modern.focus-is-visible-and-unobscured` +## 40. `modern.focus-is-visible-and-unobscured` Every focusable element shows a visible focus ring on :focus-visible, and no sticky or fixed element ever covers it. @@ -441,28 +460,7 @@ Every focusable element shows a visible focus ring on :focus-visible, and no sti - **Don't:** outline: none or 0 with no replacement; Focus rings hidden under sticky headers - **Look at:** Tab through every focusable element; diff the focused and unfocused rect or read computed outline and box-shadow under :focus-visible; check the focused rect is not intersected by position fixed or sticky elements above it. -## 40. `modern.every-state-is-designed` - -Empty, sparse, dense, error and loading states are designed; skeletons match final layout and long content never overflows. - -- **Source:** Vercel Labs, *Web Interface Guidelines — AGENTS.md* — https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/AGENTS.md -- **In their words:** "MUST: Design empty/sparse/dense/error states" -- **Do:** An empty state with a primary create action; Skeletons sized like real rows; min-w-0 and truncation on flex children -- **Don't:** Blank screens on empty arrays; Spinners that reflow content; Overflow from long strings -- **Look at:** Render with [], with 1 item, with 500 items and with a 300-character title; assert no horizontal overflow, no overlap, an actionable control in the empty state, and skeleton-to-loaded CLS under a threshold. - -## 41. `modern.numbers-and-text-do-not-shift-layout` - -Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. - -- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ -- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" -- **Do:** tabular-nums in tables, timers and prices; width and height on every img -- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover -- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. -- **Unless:** Prose numbers - -## 42. `modern.shadows-share-one-light-source` +## 41. `modern.shadows-share-one-light-source` All shadows on a page share one light direction and offset ratio, layered and tinted toward the background hue. @@ -473,7 +471,7 @@ All shadows on a page share one light direction and offset ratio, layered and ti - **Look at:** Collect every computed box-shadow; fail when offset signs mix across the page or distinct shadow strings exceed about 6; warn when shadow hue is far from the background hue with alpha above 0.5. - **Unless:** Inset shadows for sunken fields; Glows meant as glows -## 43. `modern.defaults-are-decisions-you-inherited` +## 42. `modern.defaults-are-decisions-you-inherited` A library or AI default is someone else's decision; commit to one written aesthetic direction instead of inheriting it. @@ -483,3 +481,201 @@ A library or AI default is someone else's decision; commit to one written aesthe - **Don't:** Untouched library defaults; Clean and modern as a brief; The AI-look constellation of purple gradient, Inter, identical cards, glass glow and bounce hover - **Look at:** Score the constellation: purple-to-blue/cyan gradients warn, Inter or Roboto with no display face notes, 3+ identical icon+h3+p cards note, backdrop-filter plus glow notes, gradient text on numerals warns, overshoot cubic-bezier on hover warns. - **Unless:** A purple brand is allowed to be purple; the tell is the constellation and the absence of a decision, not any one colour + +## 43. `color.status-colours-keep-their-meaning` + +Each status colour has one meaning across the product (critical for errors and blocked actions, warning for what needs attention, success for what went well, info for tips) and is never borrowed for promotion or decoration. + +- **Source:** Shopify Polaris, *Colors: Palettes and roles (Critical, Success)* — https://polaris.shopify.com/design/colors/palettes-and-roles +- **In their words:** "Elements using critical must convey messaging that implies that an action is impossible, blocked, or has resulted in an error." +- **Do:** Map error, warning, success and info to named roles or tokens and use them only in those roles; Reserve the critical red for errors, blocked actions and destructive buttons; Use the info role, not warning or critical, for tips and announcements +- **Don't:** A sale or 'new' badge in the error red; Success green used to entice or to advertise an offer; Warning colour for 'coming soon' or 'under construction' messaging; Two different reds meaning error on different screens +- **Look at:** Find the colour the page uses for error text (an element with role=alert, aria-invalid's described-by message, or a class/token named error/critical/danger) and the success colour likewise. Count painted elements (text, fill or border) whose colour equals that error or success colour, within a ΔE of 3, and that are neither a validation message, an invalid field, a status badge of that meaning, nor a destructive action. +- **Unless:** Brand colours that happen to be red, as long as a separate, distinct error red is used for errors; Data visualisations where a series colour coincides with a status hue but no status is implied (prefer avoiding it) + +## 44. `color.from-tokens-not-hex` + +Every colour on the page comes from the design system's named tokens or palette functions, never from hex values copied into components. + +- **Source:** GOV.UK Design System (Government Digital Service), *Styles: Colour* — https://design-system.service.gov.uk/styles/colour/ +- **In their words:** "Do not copy the specific hexadecimal (hex) colour values." +- **Do:** Reference colour by role token (brand, text, error, border) rather than by value; Use palette colours (tints and shades of a few families) for supporting elements; Use a functional token only in the context it is designed for +- **Don't:** Hex literals in component styles; Near-duplicate colours (#1d70b8 next to #1d70b9) created by eye-dropping; Using the error token as a general red +- **Look at:** Collect every computed color, background-color, border-*-color, outline-color and fill/stroke of painted elements, and every value of CSS custom properties declared on :root (and on any theme selector). Count distinct painted colours that match no custom-property value (exact RGBA after resolution), and count pairs of painted colours closer than ΔE 2 that are not identical. +- **Unless:** Images, illustrations and embedded third-party widgets; Browser defaults on unstyled native controls; GOV.UK: palette colours (not functional ones) are allowed for illustrations and custom components + +## 45. `color.controls-and-graphics-3-to-1` + +The parts that show a control is there and what state it is in (input borders, checkbox boxes and ticks, toggle tracks, icon-only buttons, meaningful chart marks) contrast at least 3:1 with the colours next to them. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "Unless the control is inactive, any visual information provided that is necessary for a user to identify that a control is present and how to operate it must have a minimum 3:1 contrast ratio with the adjacent colors." +- **Do:** Input borders at 3:1 against the background the input sits on, or a filled input background at 3:1; Checkbox ticks and radio dots at 3:1 against the box; Standalone icons at 3:1 against their background; Avoid very thin lines that anti-alias below the nominal ratio +- **Don't:** Pale grey input borders (#ddd on white is about 1.4:1); A selected state shown only by a faint tint; Hover effects that lower a control's contrast with its surroundings +- **Look at:** For each visible input, select, textarea, checkbox, radio, [role=switch] and icon-only button (no visible text): compute the contrast ratio between the colour that identifies it (border colour, or its own background when it has no border, or the icon fill) and the background behind it; count those below 3:1, unrounded. Disabled controls are skipped. +- **Unless:** Inactive (disabled) controls are exempt; A control identified by its visible text needs no contrasting boundary; Logos and decorative graphics; Appearance determined by the browser and not modified by the author + +## 46. `color.focus-ring-contrasts-with-its-surroundings` + +The focus indicator contrasts at least 3:1 with whatever it is drawn against: the page background for an outer ring, the component's own colours for an inner one. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast, Relationship with Focus Visible (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." +- **Do:** An outer ring that contrasts with the page background; A two-colour ring (dark and light) that holds on any background; A thick indicator rather than a 1px one +- **Don't:** A yellow outer ring on a white page; A focus border that changes hue inside the component without contrasting with its fill; Focus shown only by a background tint change +- **Look at:** Tab to each focusable control; diff the focused and unfocused screenshots of the control's box padded by a few pixels; for the changed pixels, take their colour and the colour of the unchanged pixels adjacent to them (page background outside, component fill inside). Count controls where no changed region reaches 3:1 against its adjacent colour. page.focus-visible checks only that some pixel changes; this checks that the change can be seen. +- **Unless:** Unmodified browser default focus styles; WCAG does not compare focused and unfocused states with each other; a background-only change is out of scope for 1.4.11 but fails Use of Color + +## 47. `writing.one-label-per-action` + +One concept, one word: controls that do the same thing carry the same label everywhere, and controls that do different things never share a label. + +- **Source:** GitHub Primer, *Accessibility guide: Descriptive buttons, 'How to test names'* — https://primer.style/guides/accessibility/descriptive-buttons +- **In their words:** "When buttons perform the same action, they have the same name." +- **Do:** Keep a terminology list of preferred words and words not to use for the product; Pick one verb per action (Delete or Remove, not both for the same thing) and reuse it across pages; Add the object to disambiguate repeated actions ('Remove Apples', 'Remove Pears') +- **Don't:** Synonyms for one action across screens: 'Save' here, 'Update' there, 'Apply' elsewhere; Identical labels for different actions on the same page; Naming the same object two ways ('workspace' and 'project') in one product +- **Look at:** Across the journey's pages, collect (accessible name, action) pairs for buttons and links, where action is the form action/href/handler target. Count names that map to two or more different actions on one page, and actions reached by two or more different names across pages; also flag known synonym pairs present together (save/update/apply, delete/remove, sign in/log in, cart/basket). Count must be 0. +- **Unless:** Delete and Remove may coexist when they mean different things (destroy vs take out of a collection), as Carbon defines them — then each must be used only for its own meaning + +## 48. `writing.sentence-case-ui-text` + +Labels, buttons, headings, tabs and table headers are written in sentence case; capitals only for the first word, proper nouns and acronyms — never title case or all caps for emphasis. + +- **Source:** IBM Carbon Design System, *Content guidelines: Writing style, Capitalization* — https://carbondesignsystem.com/guidelines/content/writing-style/ +- **In their words:** "Use sentence–case capitalization for all UI text elements." +- **Do:** 'First name', 'Email address', 'Save changes'; Refer to a UI element with the capitalization it has in the UI ('the My network page'); Use bold or italic, not capitals, for emphasis +- **Don't:** Title Case Buttons Like 'Save Your Changes'; ALL CAPS text set in the markup (as opposed to a tracked small label styled with text-transform); Capitalizing feature names to mark them as special +- **Look at:** For each button, label, th, tab and h1–h6 with three or more words, take the rendered text (after text-transform) and count those where two or more non-initial words that are not acronyms or in the page's proper-noun list start with a capital (title case), or where the whole text of four or more words is upper case. Count must be 0. +- **Unless:** Proper nouns, product names and acronyms keep their capitals; Mailchimp's own guide uses title case for page titles, menu names and global navigation — a house style, decide one and keep it; Short tracked overline labels in caps are a typographic choice; the four-word floor leaves them alone + +## 49. `writing.dates-numbers-units-for-the-reader` + +Dates spell out the month, numbers are numerals with thousands separators, and units sit a space after their number — formatted in the reader's locale, never as an ambiguous all-numeric date or a raw machine value. + +- **Source:** Shopify Polaris, *Content: Grammar and mechanics, 'Numbers, dates, and currency'* — https://polaris.shopify.com/content/grammar-and-mechanics +- **In their words:** "Use the month’s full name. If there isn’t enough space, use 3-letter abbreviations. Don’t write dates with numerals only." +- **Do:** 'December 11, 2024' or 'Dec 11, 2024' (in the reader's locale order); Numerals, not words: 'You have 5 orders to fulfill'; Thousands separators: '12,000'; A space between number and unit: '3.4 lb', '2 kg'; Currency code after the amount when currencies can be confused: '$10,000 USD'; Format with Intl.DateTimeFormat / Intl.NumberFormat for the user's locale +- **Don't:** All-numeric dates like '12/11/24'; ISO timestamps or epoch values shown raw ('2024-12-11T09:30:00Z'); Ordinals in dates ('January 23rd'); Unit glued to the number ('3.4lb'); Shortened numbers like '12 k' where the exact value matters +- **Look at:** Scan visible text nodes. Count matches of all-numeric dates (\b\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}\b), raw ISO timestamps (\d{4}-\d{2}-\d{2}T\d{2}:), integers of 5+ digits with no separator outside codes/IDs, and numbers glued to a unit (\d(kg|lb|cm|mm|km|mi|ml|oz)\b). Count must be 0. +- **Unless:** Polaris notes these are American English base rules and dates, numbers and measurements should be localized automatically — the target is the reader's locale, not US format; Identifiers, codes, SKUs and years are not quantities and take no separator; Dense data tables may use compact numeric dates if the format is unambiguous for the locale and stated in the column header + +## 50. `data-display.no-tables-for-layout` + +A table is for comparing data in rows and columns, never for arranging content on the page; layout belongs to the grid. + +- **Source:** GOV.UK Design System (Government Digital Service), *Table — When not to use this component* — https://design-system.service.gov.uk/components/table/ +- **In their words:** "Never use the table component to layout content on a page." +- **Do:** CSS grid or flex for page and dashboard layout; A list, cards or a summary list for items that do not share columns; Tables only where every row has the same fields +- **Don't:** A table that positions a sidebar, form or dashboard tiles; Table cells holding headings, paragraphs or whole forms; role=presentation on a table that actually holds tabular data +- **Look at:** Count table elements that have no th and either contain headings, more than one paragraph per cell, form fieldsets or nested tables, or have a single row whose cells hold unrelated content blocks. +- **Unless:** HTML email, where tables are still the only reliable layout tool + +## 51. `data-display.text-left-headers-follow-their-column` + +Text cells are left-aligned, nothing in a data table is centred, and each column header takes the same alignment as the data below it. + +- **Source:** W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.), *Tables Tutorial — Tips and Tricks: Alignment* — https://www.w3.org/WAI/tutorials/tables/tips/ +- **In their words:** "Align text to the left and numeric data to the right (in left-to-right languages), so that people using larger text sizes or smaller screens will be able to find it. … It’s helpful to give column headers the same alignment as the data in the cells below." +- **Do:** Left-align (start-align) textual cells and their headers; Give each th the same horizontal alignment as the cells of its column; Mirror alignment in right-to-left languages +- **Don't:** Centre-aligned text or number columns; A left-aligned header above a right-aligned numeric column; Centred headers as a table-wide default +- **Look at:** For each table column: count th whose computed text-align differs from the dominant text-align of the td in the same column, and count td/th whose computed text-align is center (excluding cells that hold only a checkbox, icon or status badge). +- **Unless:** A column holding only a checkbox, icon or single glyph may be centred; Right-to-left scripts reverse the sides + +## 52. `data-display.one-unit-and-format-per-column` + +Every value in a column uses the same unit, precision and format; the unit is named once in the header, not repeated in each cell. + +- **Source:** U.S. Web Design System (GSA), *Table — Usability guidance* — https://designsystem.digital.gov/components/table/ +- **In their words:** "Predictably format columns. Take care not to vary units or formatting within the same column. Instead, normalize values so they can be easily compared." +- **Do:** Normalise a column to one unit (all days, not days and weeks); Keep the same number of decimal places in every row of a column; Put the unit or symbol in the column header ('Temperature °C') rather than in every cell; Use one date format and one thousands separator throughout the table +- **Don't:** Mixing '3 days' and '2 weeks' in one column; '12.5', '12.50' and '12.500' in the same column; Abbreviating some values ('1.2k') and not others in the same column +- **Look at:** For each column of numeric td, count distinct decimal-place counts, distinct unit suffixes or prefixes, and distinct date formats; any column with more than one is a failure. +- **Unless:** A column deliberately mixing magnitudes where the unit changes are labelled per cell and sorting is by underlying value — rare, and worth questioning + +## 53. `data-display.summary-list-for-key-value-facts` + +A set of facts about one thing — label and value pairs — is shown as a summary list (dl with dt and dd), not as a table with no headers or as loose text. + +- **Source:** GOV.UK Design System (Government Digital Service), *Summary list — When to use / When not to use this component* — https://design-system.service.gov.uk/components/summary-list/ +- **In their words:** "Use a summary list to show information as a list of key facts. … only use it to present information that has a key and at least one value." +- **Do:** dl with dt (the key) and dd (the value) for record details, metadata and check-your-answers pages; A row action ('Change') whose accessible name includes the key ('Change name'); Headings or cards to separate several summary lists on one page +- **Don't:** A two-column table without th used to show one record's fields; Key–value pairs set as 'Label: value' runs in a paragraph; A summary list for genuinely tabular data or a plain list of items +- **Look at:** Where the screen shows the fields of a single record (a profile, an order, an item's metadata, a check-answers page), are the label–value pairs marked up as dl/dt/dd with each key visually distinct from its value — and is tabular data comparing several records in a table rather than a summary list? +- **Unless:** Two or three facts inside a card may be inline text if they are not scanned as a set + +## 54. `forms.error-summary-at-the-top` + +After a failed submit, show an error summary at the top of the page that takes focus and links each error to its field — even when there is only one error. + +- **Source:** GOV.UK Design System, *Error summary component* — https://design-system.service.gov.uk/components/error-summary/ +- **In their words:** "Always show an error summary when there is a validation error, even if there’s only one." +- **Do:** A summary above the h1 (below any back link) with a heading such as 'There is a problem'; Move keyboard focus to the summary when it appears; One link per error, pointing at the field (or the first field of a date or radio group); Word each summary item exactly like the message beside its field; Prefix the page with 'Error: ' +- **Don't:** Errors shown only beside fields far down a long form, with focus left on the submit button; A summary of plain text with no links to the fields; A toast or banner that disappears before it can be read +- **Look at:** Submit the form empty. Within 1 s: is there an element above the first h1 of main that has focus (document.activeElement inside it), contains one link per invalid field, and does each link's href resolve to the id of an invalid input? Does document.title start with 'Error'? +- **Unless:** Primer suggests the interactive summary only for 3 or more errors, and otherwise focusing the first invalid field; A one-field form (search, single email sign-up) can rely on the message beside the field + +## 55. `forms.validate-when-the-user-is-done` + +Do not show an error while the user is still typing; validate when they try to continue, and only add earlier validation where research shows it helps. + +- **Source:** GOV.UK Design System, *Recover from validation errors pattern — When to tell the user about validation errors* — https://design-system.service.gov.uk/patterns/validation/ +- **In their words:** "Generally speaking, avoid validating the information in a field before the user has finished entering it. This sort of validation can cause problems - especially for users who type more slowly." +- **Do:** Validate on Continue or Submit; After a failed submit, update a field's error live once the user fixes it (Primer); A character count is the accepted exception: warn as the limit is passed +- **Don't:** An error that appears on the first keystroke of an email or phone field; Red borders on an untouched form at load; Browser-native HTML5 validation bubbles in place of designed messages +- **Look at:** Focus an email or formatted field and type one character, keeping focus. Wait 1 s. Does any error message, aria-invalid=true or error colour appear before blur or submit? Also load the form fresh: are any fields already marked invalid? +- **Unless:** GOV.UK goes further and says not to validate on blur either; Baymard (usability.inline-validation-after-leaving-field) recommends blur validation for hard fields — the schools agree only on 'not while typing'; Primer allows validating as the user types once the field has already been flagged invalid, so the error clears as soon as it is fixed + +## 56. `forms.keep-answers-after-an-error` + +When a submit fails, show the form again with every answer the user gave still in it — the failing ones and the passing ones. + +- **Source:** GOV.UK Design System, *Error message component — How it works* — https://design-system.service.gov.uk/components/error-message/ +- **In their words:** "Do not clear any form fields when showing the Error message component. Keep both passing and failing answers." +- **Do:** Re-render server-side errors with submitted values filled in; Keep the failing value so the user can see and edit what went wrong; Pre-populate fields when the user goes back to change an answer +- **Don't:** A reload that empties the form after a server error; Clearing the field that failed; Clearing password-adjacent fields such as name and email along with the password +- **Look at:** Fill every field with valid values except one, submit, and after the error page paints read each input's value: how many fields that had a value now read empty? +- **Unless:** Password and card security code fields may be cleared for security, and the message should say so + +## 57. `forms.mark-optional-fields-in-words` + +Ask mostly required questions and label the exceptions '(optional)' in words; do not mark required fields with asterisks. + +- **Source:** GOV.UK Design System, *Question pages pattern* — https://design-system.service.gov.uk/patterns/question-pages/ +- **In their words:** "in most contexts, add ‘(optional)’ to the labels of optional fields" +- **Do:** '(optional)' in the label, or in the legend for a radio or checkbox group; Removing optional questions before marking them; One convention across the whole service +- **Don't:** Asterisks on mandatory fields; Optional fields that look identical to required ones; Required and optional marked differently on different pages +- **Look at:** Submit the form with every field empty and note which fields raise no error (optional). Count optional fields whose label or legend text lacks 'optional', and labels that contain '*'. +- **Unless:** USWDS instead marks required fields with a red asterisk explained by a note at the top, and also labels optional fields '(optional)' — the schools agree that optional fields say so in words; Primer and USWDS exempt one-field forms and login forms, where every field is plainly required + +## 58. `forms.autocomplete-names-the-purpose` + +Every field that asks about the user — name, email, phone, address, postcode, birthday, card — carries the matching autocomplete token so browsers and assistive tech can fill and label it. + +- **Source:** W3C Web Accessibility Initiative, *Understanding WCAG 2.2 Success Criterion 1.3.5: Identify Input Purpose* — https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html +- **In their words:** "Use code to indicate the purpose of common inputs, where technology allows." +- **Do:** autocomplete='name' or 'given-name' and 'family-name' on name fields; 'email', 'tel', 'postal-code', 'street-address' or 'address-line1', 'bday-day'/'bday-month'/'bday-year'; 'shipping' and 'billing' section tokens on order forms; autocomplete='off' on the form, if needed, while each field still declares its purpose +- **Don't:** autocomplete='off' on personal fields with no purpose token; Personal-data fields with no autocomplete attribute; A token that does not match the field (email token on a phone field) +- **Look at:** For every input, select and textarea whose label matches name, email, phone, address, postcode/ZIP, city, country, date of birth, card number or expiry: count those whose autocomplete attribute is missing, 'off', or not a WCAG input-purpose token that matches the label. +- **Unless:** Fields about someone other than the user (a recipient's email) are outside 1.3.5; A field that accepts either username or email may carry one token or none + +## 59. `forms.hint-text-is-short-and-linked` + +Hint text sits between the label and the input, is one short sentence with no links, and is tied to the input with aria-describedby; longer explanation goes in the page body. + +- **Source:** GOV.UK Design System, *Text input component — Hint text* — https://design-system.service.gov.uk/components/text-input/ +- **In their words:** "Keep hint text to a single short sentence, without any full stops." +- **Do:** Hint placed after the label and before the input; The input's aria-describedby lists the hint's id (and the error's, when shown); Hints that say where to find the answer or how it will be used; A statement heading plus body text when a question needs a long explanation +- **Don't:** Links inside hint text; Multi-sentence paragraphs as hints; Hints placed below the input where they are read after typing; Hints in a tooltip behind an icon +- **Look at:** For every element used as a hint (referenced by an input's aria-describedby, or styled as hint between label and input): count hints containing an <a>, hints longer than one sentence or about 120 characters, hints rendered below their input, and visible hints not referenced by their input's aria-describedby. +- **Unless:** A format example ('For example, QQ 12 34 56 C') may follow the sentence; "About 120 characters" in "look at" is uxcli's reading of "a single short sentence", not a number the source gives. + +## 60. `forms.ask-only-what-you-need` + +Every question on the form has a named use; drop the ones nobody can justify, and never ask for the same thing twice in one journey. + +- **Source:** GOV.UK Design System, *Question pages pattern* — https://design-system.service.gov.uk/patterns/question-pages/ +- **In their words:** "You should make sure you know why you’re asking every question and only ask users for information you really need." +- **Do:** A question protocol: for each field, who uses the answer and what for; Reuse an earlier answer by pre-filling it rather than asking again; Let 'I do not know' be an answer where it is a valid one; Say why a sensitive question is asked, in its hint +- **Don't:** 'Nice to have' fields such as title, gender or 'how did you hear about us' with no stated use; Asking for an email or address a second time in the same journey; Confirm-email fields that make the user type the address twice, instead of playing it back for checking +- **Look at:** List every field on the form. For each, can the team name who reads the answer and what decision it changes? How many fields have no answer, and how many repeat information given earlier in the journey? +- **Unless:** Legal or regulatory questions whose use is mandated, which should still say why in a hint diff --git a/.claude/skills/uxcli/lenses/transaction.md b/.claude/skills/uxcli/lenses/transaction.md index cbf3df8..177939a 100644 --- a/.claude/skills/uxcli/lenses/transaction.md +++ b/.claude/skills/uxcli/lenses/transaction.md @@ -2,7 +2,7 @@ Checkout, sign-up, booking, onboarding and multi-step forms: one task, completed once, correctly. -44 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. +61 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. ## 1. `usability.fitts-target-size-and-distance` @@ -16,16 +16,17 @@ Make targets big, space them apart, and put them where the pointer already is; t - **Unless:** Infinite targets along screen edges for mouse — size matters less at an edge; Primary CTAs, moving users, children and the elderly need larger than the minimum - **Also stated as:** usability.hicks-fewer-choices-when-time-matters (Jon Yablonski (Laws of UX)); usability.peak-end-finish-well (Jon Yablonski (Laws of UX)); usability.von-restorff-one-emphasis (GOV.UK Design System); craft.growth-design-psychology-principles (Growth.Design (Dan Benoni, Louis-Xavier Lavallée)); modern.no-deceptive-patterns (Harry Brignull). -## 2. `usability.no-false-affordances` +## 2. `usability.speak-the-users-language` -Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. +Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. -- **Source:** Hoa Loranger, NN/g, *Beyond Blue Links: Making Clickable Elements Recognizable (2015)* — https://www.nngroup.com/articles/clickable-elements/ -- **In their words:** "Avoid making non-clickable items (like headings) resemble buttons." -- **Do:** Reserve link colour and underline for links; Reserve button shape and fill for buttons -- **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons -- **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). -- **Also stated as:** canon.rams-honest (Dieter Rams). +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #2* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "The design should speak the users' language. Use words, phrases, and concepts familiar to the user, rather than internal jargon." +- **Do:** Use words the user understands without looking them up; Spell acronyms out in full on each page; Write 'for example', not 'eg' or 'ie' +- **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations +- **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. +- **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se +- **Also stated as:** writing.errors-say-what-and-how-to-fix (GOV.UK Design System (Government Digital Service)); writing.plain-language-reading-level (Shopify Polaris); forms.error-message-says-how-to-fix (GOV.UK Design System); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen). ## 3. `usability.inline-validation-after-leaving-field` @@ -37,42 +38,43 @@ Validate a hard field after the user leaves it, never on focus or while typing, - **Don't:** Validate before and while typing; Show an error on focus of an empty field; Validate only on submit - **Look at:** Drive a field: focus shows no error; typing an invalid value shows none (unless length threshold reached); blur shows an error adjacent via aria-describedby; typing a fix clears it without blur. - **Unless:** Simple fields need none; premature validation was worse than none (Wroblewski) -- **Also stated as:** usability.visibility-of-system-status (Jakob Nielsen); modern.feedback-is-local-and-optimistic (Rauno Freiberg). +- **Also stated as:** usability.visibility-of-system-status (Jakob Nielsen); modern.feedback-is-local-and-optimistic (Rauno Freiberg); forms.error-summary-at-the-top (GOV.UK Design System). -## 4. `usability.feedback-within-a-second` +## 4. `usability.postel-tolerant-input` -Paint something within 0.1 s; keep the user's flow with a response under 1 s; show a progress indicator for anything longer. +Accept every reasonable form of an input — spaces, dashes, case, whitespace — and normalise it, instead of rejecting it. -- **Source:** Jakob Nielsen, *Response Times: The 3 Important Limits (1993)* — https://www.nngroup.com/articles/response-times-3-important-limits/ (study) -- **In their words:** "0.1 second is about the limit for having the user feel that the system is reacting instantaneously … 1.0 second is about the limit for the user's flow of thought to stay uninterrupted" -- **Do:** Use a progress indicator for any action over about 1 s; Use a looped indicator for 2–9 s waits; Use a percent-done indicator for 10 s or more -- **Don't:** Leave dead time with no indicator; Show a percent-done bar that lies badly -- **Look at:** Time from input event to first paint of any change; presence of a progress element, aria-busy, <progress> or role=status update when the wait exceeds 1 s. -- **Unless:** Laws of UX claims a purposeful delay can raise perceived value — an opinion with no study cited -- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg). +- **Source:** Jon Yablonski (Laws of UX), *Postel's Law — Laws of UX* — https://lawsofux.com/postels-law/ +- **In their words:** "Be liberal in what you accept, and conservative in what you send." +- **Do:** Accept variable input and translate it to your format; Normalise on blur; Define boundaries and give clear feedback +- **Don't:** Reject spaces or dashes in card or phone numbers; Reject leading or trailing whitespace in email; Treat email as case-sensitive; Force a date format the field could parse +- **Look at:** Drive each formatted field with equivalent variants ('4111 1111 1111 1111' vs '4111111111111111', ' a@b.co ', '+44 20…' vs '02…') and compare validation outcomes. +- **Unless:** Inputs where ambiguity is dangerous, such as dates in medical or legal contexts +- **Also stated as:** craft.body-16px-line-height-1-5 (Steve Schoger); modern.mobile-inputs-do-not-zoom-or-trap (Rauno Freiberg); forms.input-type-matches-the-answer (GOV.UK Design System). -## 5. `usability.signifiers-make-clickable-look-clickable` +## 5. `usability.omit-needless-words` -Links and buttons must look clickable — colour, underline, border or fill — because weak signifiers cost measured time and fixations. +Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. -- **Source:** Kate Moran, NN/g, *Flat UI Elements Attract Less Attention and Cause Uncertainty (2017)* — https://www.nngroup.com/articles/flat-ui-less-attention-cause-uncertainty/ (study) -- **In their words:** "22% more time" -- **Do:** Make links stand out from body text; Make buttons resemble physical buttons with rectangular shapes; Apply consistent treatment throughout the site; Provide signifiers rather than rely on affordances -- **Don't:** Style linked text as static text; Use ghost buttons as the default; Use disabled buttons if avoidable; Rely on a label to say that a control is a control -- **Look at:** For each a[href], button, [role=button]: computed colour, underline, border and background versus surrounding text; a link matching body colour with no underline and no other differentiator is weak; count disabled buttons. -- **Unless:** Link position in nav menus or peripheral lists may eliminate the need for underlining (Loranger); Flat works best with low information density, traditional layouts and high-contrast targets positioned standardly (Moran) -- **Also stated as:** modern.signifiers-survive-flatness (Kate Moran (NN/G)). +- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) +- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" +- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns +- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case +- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. +- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text +- **Also stated as:** writing.sentence-case-ui-text (IBM Carbon Design System). -## 6. `usability.speak-the-users-language` +## 6. `usability.fewer-checkout-fields` -Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. +Count fields, not steps: a guest checkout needs about eight, with optional fields collapsed behind links and billing defaulted to shipping. -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #2* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "The design should speak the users' language. Use words, phrases, and concepts familiar to the user, rather than internal jargon." -- **Do:** Use words the user understands without looking them up; Spell acronyms out in full on each page; Write 'for example', not 'eg' or 'ie' -- **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations -- **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. -- **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se +- **Source:** Baymard Institute, *Checkout Optimization: minimize form fields (2024)* — https://baymard.com/blog/checkout-flow-average-form-fields (study) +- **In their words:** "The number of form fields in a checkout impacts overall usability far more than the number of steps." +- **Do:** Consolidate to a single 'Full Name' field; Hide Address Line 2 and the coupon field behind links; Default billing to shipping; Offer account creation after purchase +- **Don't:** Count steps as the metric; Show every optional field by default +- **Look at:** Count visible input, select and textarea across the checkout (excluding hidden and collapsed); optional fields shown expanded; separate first/last name fields; an open coupon field. +- **Unless:** Regulatory or fraud fields that genuinely cannot be defaulted; The minimum of 8 assumes a standard guest checkout +- **Also stated as:** forms.mark-optional-fields-in-words (GOV.UK Design System); forms.ask-only-what-you-need (GOV.UK Design System). ## 7. `usability.minimalist-no-competing-information` @@ -85,29 +87,65 @@ Everything on the screen competes with the primary goal; remove what does not se - **Look at:** At 360 px: count distinct interactive elements above the fold; whether the journey's primary action is within the first viewport; count of elements sharing the accent colour. - **Unless:** Dense expert dashboards where the irreducible information is large -## 8. `usability.postel-tolerant-input` +## 8. `usability.no-false-affordances` -Accept every reasonable form of an input — spaces, dashes, case, whitespace — and normalise it, instead of rejecting it. +Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. -- **Source:** Jon Yablonski (Laws of UX), *Postel's Law — Laws of UX* — https://lawsofux.com/postels-law/ -- **In their words:** "Be liberal in what you accept, and conservative in what you send." -- **Do:** Accept variable input and translate it to your format; Normalise on blur; Define boundaries and give clear feedback -- **Don't:** Reject spaces or dashes in card or phone numbers; Reject leading or trailing whitespace in email; Treat email as case-sensitive; Force a date format the field could parse -- **Look at:** Drive each formatted field with equivalent variants ('4111 1111 1111 1111' vs '4111111111111111', ' a@b.co ', '+44 20…' vs '02…') and compare validation outcomes. -- **Unless:** Inputs where ambiguity is dangerous, such as dates in medical or legal contexts +- **Source:** Hoa Loranger, NN/g, *Beyond Blue Links: Making Clickable Elements Recognizable (2015)* — https://www.nngroup.com/articles/clickable-elements/ +- **In their words:** "Avoid making non-clickable items (like headings) resemble buttons." +- **Do:** Reserve link colour and underline for links; Reserve button shape and fill for buttons +- **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons +- **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). +- **Also stated as:** canon.rams-honest (Dieter Rams). -## 9. `usability.omit-needless-words` +## 9. `usability.help-in-context` -Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. +The best help is none; when a field needs explaining, put a short hint beside it at the moment it is needed, not on another page. -- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) -- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" -- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns -- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case -- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. -- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #10* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "Whenever possible, present the documentation in context right at the moment that the user requires it." +- **Do:** Use hint text for help relevant to most users; Keep hint text to one short sentence without full stops; Associate the hint with its input via aria-describedby +- **Don't:** Put help only on a separate page; Put links inside hint text; Write lengthy explanations +- **Look at:** For inputs with a format expectation (date, postcode, card): a hint associated via aria-describedby and rendered adjacent, same column, within one line-height; hint length in sentences. +- **Unless:** Nielsen's own first sentence: it is best if the system needs no additional explanation +- **Also stated as:** usability.placeholders-are-not-labels (Katie Sherwin, NN/g); forms.hint-text-is-short-and-linked (GOV.UK Design System). + +## 10. `usability.feedback-within-a-second` -## 10. `usability.progress-indication-in-flows` +Paint something within 0.1 s; keep the user's flow with a response under 1 s; show a progress indicator for anything longer. + +- **Source:** Jakob Nielsen, *Response Times: The 3 Important Limits (1993)* — https://www.nngroup.com/articles/response-times-3-important-limits/ (study) +- **In their words:** "0.1 second is about the limit for having the user feel that the system is reacting instantaneously … 1.0 second is about the limit for the user's flow of thought to stay uninterrupted" +- **Do:** Use a progress indicator for any action over about 1 s; Use a looped indicator for 2–9 s waits; Use a percent-done indicator for 10 s or more +- **Don't:** Leave dead time with no indicator; Show a percent-done bar that lies badly +- **Look at:** Time from input event to first paint of any change; presence of a progress element, aria-busy, <progress> or role=status update when the wait exceeds 1 s. +- **Unless:** Laws of UX claims a purposeful delay can raise perceived value — an opinion with no study cited +- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg). + +## 11. `usability.signifiers-make-clickable-look-clickable` + +Links and buttons must look clickable — colour, underline, border or fill — because weak signifiers cost measured time and fixations. + +- **Source:** Kate Moran, NN/g, *Flat UI Elements Attract Less Attention and Cause Uncertainty (2017)* — https://www.nngroup.com/articles/flat-ui-less-attention-cause-uncertainty/ (study) +- **In their words:** "22% more time" +- **Do:** Make links stand out from body text; Make buttons resemble physical buttons with rectangular shapes; Apply consistent treatment throughout the site; Provide signifiers rather than rely on affordances +- **Don't:** Style linked text as static text; Use ghost buttons as the default; Use disabled buttons if avoidable; Rely on a label to say that a control is a control +- **Look at:** For each a[href], button, [role=button]: computed colour, underline, border and background versus surrounding text; a link matching body colour with no underline and no other differentiator is weak; count disabled buttons. +- **Unless:** Link position in nav menus or peripheral lists may eliminate the need for underlining (Loranger); Flat works best with low information density, traditional layouts and high-contrast targets positioned standardly (Moran) +- **Also stated as:** modern.signifiers-survive-flatness (Kate Moran (NN/G)). + +## 12. `usability.clearly-marked-emergency-exit` + +Every interaction has a visible, labelled way out — Cancel, Back, Escape, Undo — so a mistaken action does not trap the user. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #3* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "Users often perform actions by mistake. They need a clearly marked 'emergency exit' to leave the unwanted action without having to go through an extended process." +- **Do:** Support Undo and Redo; Show a clear Cancel or close control; Label the exit clearly and make it discoverable +- **Don't:** Open a modal with no close; Build a wizard step with no back; Ship a destructive action with no undo +- **Look at:** For each role=dialog or modal: a focusable control whose text or aria-label matches close/cancel/back, and Escape dismisses it; for each step past the first in a flow: a back control exists. +- **Unless:** Legally required interstitials — the exit must still be visible, not necessarily free of consequence + +## 13. `usability.progress-indication-in-flows` In flows of three or more steps give some sense of position; do not assume a step bar helps — GOV.UK removed one with no effect. @@ -118,7 +156,7 @@ In flows of three or more steps give some sense of position; do not assume a ste - **Look at:** Presence of a step or position element in flows with 3 or more steps, and whether it updates between steps. - **Unless:** Short flows — GOV.UK's own guidance omits indicators by default; Artificial 'endowed progress' edges toward manipulation — flag, do not recommend -## 11. `usability.single-column-forms` +## 14. `usability.single-column-forms` Forms run in one vertical column; only coherent entities like city/state/ZIP or card/expiry/CVV share a row. @@ -129,18 +167,7 @@ Forms run in one vertical column; only coherent entities like city/state/ZIP or - **Look at:** Cluster inputs by left x-coordinate; more than one column of independent inputs, not in the same fieldset or an allowed coherent row, fails. - **Unless:** Coherent-entity rows: city/state/ZIP and card number/expiry/security code -## 12. `usability.fewer-checkout-fields` - -Count fields, not steps: a guest checkout needs about eight, with optional fields collapsed behind links and billing defaulted to shipping. - -- **Source:** Baymard Institute, *Checkout Optimization: minimize form fields (2024)* — https://baymard.com/blog/checkout-flow-average-form-fields (study) -- **In their words:** "The number of form fields in a checkout impacts overall usability far more than the number of steps." -- **Do:** Consolidate to a single 'Full Name' field; Hide Address Line 2 and the coupon field behind links; Default billing to shipping; Offer account creation after purchase -- **Don't:** Count steps as the metric; Show every optional field by default -- **Look at:** Count visible input, select and textarea across the checkout (excluding hidden and collapsed); optional fields shown expanded; separate first/last name fields; an open coupon field. -- **Unless:** Regulatory or fraud fields that genuinely cannot be defaulted; The minimum of 8 assumes a standard guest checkout - -## 13. `usability.aesthetic-usability-effect-bias` +## 15. `usability.aesthetic-usability-effect-bias` Polish makes a screen look more usable than it is; discount your aesthetic impression and run the measurable checks first. @@ -151,28 +178,6 @@ Polish makes a screen look more usable than it is; discount your aesthetic impre - **Look at:** Did the evaluator judge the screen usable because it looks good? Re-check the verdict against the count-kind entries before trusting it; 'attractive' is the variable and stays taste. - **Unless:** With severe usability issues, or functionality sacrificed for aesthetics, users lose patience -## 14. `usability.clearly-marked-emergency-exit` - -Every interaction has a visible, labelled way out — Cancel, Back, Escape, Undo — so a mistaken action does not trap the user. - -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #3* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "Users often perform actions by mistake. They need a clearly marked 'emergency exit' to leave the unwanted action without having to go through an extended process." -- **Do:** Support Undo and Redo; Show a clear Cancel or close control; Label the exit clearly and make it discoverable -- **Don't:** Open a modal with no close; Build a wizard step with no back; Ship a destructive action with no undo -- **Look at:** For each role=dialog or modal: a focusable control whose text or aria-label matches close/cancel/back, and Escape dismisses it; for each step past the first in a flow: a back control exists. -- **Unless:** Legally required interstitials — the exit must still be visible, not necessarily free of consequence - -## 15. `usability.help-in-context` - -The best help is none; when a field needs explaining, put a short hint beside it at the moment it is needed, not on another page. - -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #10* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "Whenever possible, present the documentation in context right at the moment that the user requires it." -- **Do:** Use hint text for help relevant to most users; Keep hint text to one short sentence without full stops; Associate the hint with its input via aria-describedby -- **Don't:** Put help only on a separate page; Put links inside hint text; Write lengthy explanations -- **Look at:** For inputs with a format expectation (date, postcode, card): a hint associated via aria-describedby and rendered adjacent, same column, within one line-height; hint length in sentences. -- **Unless:** Nielsen's own first sentence: it is best if the system needs no additional explanation - ## 16. `usability.natural-mapping-and-proximity` Put a control next to the thing it changes and a label next to its field; gaps between groups must exceed gaps within them. @@ -184,18 +189,7 @@ Put a control next to the thing it changes and a label next to its field; gaps b - **Look at:** Distance from each label to its own input versus the nearest other input; gap within a fieldset versus gap between fieldsets; distance from last input to the submit button. - **Unless:** Left-aligned labels are acceptable if space is constrained and labels are of similar length -## 17. `usability.placeholders-are-not-labels` - -Every input has a visible label outside the field; placeholder text is never the label, the hint or the only format example. - -- **Source:** Katie Sherwin, NN/g, *Placeholders in Form Fields Are Harmful (2014)* — https://www.nngroup.com/articles/form-design-placeholders/ -- **In their words:** "Disappearing placeholder text strains users' short-term memory." -- **Do:** Give every text input a visible label; Put hints outside empty form fields -- **Don't:** Use placeholder text in place of a label; Use placeholder text for hints or examples; Hide the label visually and rely on the placeholder -- **Look at:** Any input or textarea with a placeholder and no visible associated label, or with a visually hidden label; placeholder used as the only format example. -- **Unless:** A single-field search where the button label carries the meaning is the usual carve-out; the fetched page states none explicitly - -## 18. `usability.labels-above-fields` +## 17. `usability.labels-above-fields` Stack the label above its field, left edges aligned, a few pixels apart, so label and field are read in one fixation. @@ -206,7 +200,7 @@ Stack the label above its field, left edges aligned, a few pixels apart, so labe - **Look at:** For each label/input pair: label box bottom ≤ input box top and label left edge ≈ input left edge within a few px; if side by side, the gap between label right edge and input left edge. - **Unless:** Landscape phones: switch to left-aligned to keep the field visible above the keyboard (Baymard); Left-aligned is acceptable when labels are of similar length and as close to the fields as possible (NN/g) -## 19. `usability.field-width-matches-expected-input` +## 18. `usability.field-width-matches-expected-input` Size each input to the data it expects: short boxes for year, postcode and CVV; one consistent width for variable data like email. @@ -217,7 +211,7 @@ Size each input to the data it expects: short boxes for year, postcode and CVV; - **Look at:** Rendered input width in ch of its font versus maxlength, inputmode, autocomplete token or pattern length: cc-csc wider than ~8 ch, postal-code at full width, or email narrower than ~20 ch. - **Unless:** Full-width inputs on narrow phones are the norm — check at desktop widths or compare relative widths within the form -## 20. `usability.one-thing-per-page` +## 19. `usability.one-thing-per-page` Split a public-facing form so each page asks one question, decision or piece of information, with eligibility questions first. @@ -228,7 +222,7 @@ Split a public-facing form so each page asks one question, decision or piece of - **Look at:** Count distinct questions (label groups or fieldsets) per page in a flow; count required inputs per page. - **Unless:** Be consistent, not uniform: internal expert tools and repeat users may prefer denser pages -## 21. `usability.dont-make-me-think` +## 20. `usability.dont-make-me-think` A page's purpose and controls should be self-evident; nothing on it should raise a question the user must answer before acting. @@ -238,7 +232,7 @@ A page's purpose and controls should be self-evident; nothing on it should raise - **Don't:** Raise a question the user must answer before acting - **Look at:** Look at the screen cold: is there anything you have to work out before you can act? The law is the sum of the other entries; a machine measures its symptoms, not the law. -## 22. `usability.start-with-user-needs-design-with-data` +## 21. `usability.start-with-user-needs-design-with-data` Name the actor and their need before drawing a screen, then let measured behaviour, not hunches or looks, decide what changes. @@ -248,7 +242,7 @@ Name the actor and their need before drawing a screen, then let measured behavio - **Don't:** Design to a hunch; Design to an aesthetic - **Look at:** Does the journey name its actor and need (uxcli context show), and is the design decision traceable to observed behaviour rather than a hunch or a look? -## 23. `usability.constraints-shift-complexity-to-the-system` +## 22. `usability.constraints-shift-complexity-to-the-system` Complexity that cannot be removed goes to the system, not the user: default what can be inferred, hide what is rarely needed. @@ -259,7 +253,7 @@ Complexity that cannot be removed goes to the system, not the user: default what - **Look at:** Count visible required inputs the page could default (no 'same as shipping' control, both address blocks open); presence of autocomplete tokens on address and payment inputs. - **Unless:** Tognazzini's counter-view: users resist complexity reduction and attempt harder tasks when systems get simpler -## 24. `craft.fewer-borders` +## 23. `craft.fewer-borders` Separate elements with space, a background shift, a shadow or striping before reaching for a border; too many borders make a design busy. @@ -271,7 +265,7 @@ Separate elements with space, a background shift, a shadow or striping before re - **Unless:** Form inputs and the one object a person acts on keep their border; Keylines that make disconnected content feel connected; Decorative borders as style once the count is low - **Also stated as:** canon.gestalt-common-region (Aurora Harley, NN/g); canon.rams-as-little-design-as-possible (Dieter Rams); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); canon.tufte-smallest-effective-difference (Edward Tufte); canon.tufte-one-plus-one-equals-three (Edward Tufte). -## 25. `craft.spacing-and-sizing-system` +## 24. `craft.spacing-and-sizing-system` Draw every margin, padding and gap from one scale, and make space between groups clearly larger than space within them. @@ -281,9 +275,9 @@ Draw every margin, padding and gap from one scale, and make space between groups - **Don't:** Use one-off values such as 13px or 27px; Use equal gaps across a group boundary - **Look at:** Collect all computed margin, padding and gap values above 0; report distinct values and how many are off a 4px grid; compare label-to-field gap with field-to-next-label gap. - **Unless:** Optical adjustments on icons and hanging punctuation sit a pixel or two off-scale -- **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen); modern.spacing-comes-from-a-scale (Stan Kirilov). +- **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.spacing-comes-from-a-scale (Stan Kirilov). -## 26. `craft.separation-order-space-then-lines-then-boxes` +## 25. `craft.separation-order-space-then-lines-then-boxes` Use the lightest separator that works: more space first, then a keyline or background band, and a box only for the object that is acted on. @@ -293,43 +287,7 @@ Use the lightest separator that works: more space first, then a keyline or backg - **Look at:** Between sibling groups, record which separator is used: gap at least 2x the inner gap (space), hr or border-bottom (line), bordered or shadowed wrapper (box); report the inner/outer gap ratio. - **Unless:** Dense data such as tables where zebra stripes or keylines are used; Interactive cards that are the unit of action -## 27. `craft.body-16px-line-height-1-5` - -Body copy at 16px or more with 1.5 line height; inputs at 16px or more on mobile. - -- **Source:** Steve Schoger, *Little UI Details (tweet, 1 Jun 2017)* — https://digitalsynopsis.com/design/useful-ui-ux-design-tips/ -- **In their words:** "If in doubt, 16px font with 1.5 line height is pretty good safe for body copy." -- **Do:** Set body at 16px or more, secondary about 2px smaller; Set inputs at 16px or more on mobile; Set text-heavy desktop pages at 18–24px -- **Don't:** Set body text under 16px on phones; Set inputs under 16px on iOS -- **Look at:** At a 375px viewport, computed font-size of paragraph text and of input, textarea and select; line-height divided by font-size for paragraphs. -- **Unless:** Interaction-heavy desktop pages may go to 14px; Captions sit 2px under body by design -- **Also stated as:** modern.readable-type-sizes-and-weights (Rauno Freiberg); modern.mobile-inputs-do-not-zoom-or-trap (Rauno Freiberg). - -## 28. `craft.tap-targets-and-control-height` - -Touch targets are at least 44×44; inputs and the buttons beside them share one height of 40 or 48px. - -- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html -- **In their words:** "The minimum tap target size on both iOS and Android — On iOS: 44x44pt. On Android: 48x48pt." -- **Do:** Give touch viewports hit areas of 44×44 or more; Match button height to the inputs beside it -- **Don't:** Use 26px-wide grid cells as targets; Make buttons shorter than the inputs they sit beside -- **Look at:** At 375px, getBoundingClientRect of every a, button, input and role=button: fail under 44 in width or height (under 24 for inline text links); in a form row, button and input heights within 2px. -- **Unless:** Inline text links in running prose; Dense desktop-only tools if the project commits to no touch -- **Also stated as:** modern.hit-targets-meet-platform-minimums (Apple). - -## 29. `craft.type-scale-few-font-sizes` - -Use about four font sizes from a fixed scale; reuse the default size for body, menus, lists and controls. - -- **Source:** Erik D. Kennedy, *The Responsive Website Font Size Guidelines* — https://www.learnui.design/blog/mobile-desktop-website-font-size-guidelines.html -- **In their words:** "Even the most interaction-heavy pages can typically look just fine with about 4 font sizes total." -- **Do:** Use header, default, secondary (default minus 2px) and one wildcard size; Reuse the default size across body, menus, lists and controls -- **Don't:** Invent a new size per component; Apply a strict modular or golden-ratio scale to responsive pages -- **Look at:** Count distinct computed font-size values on visible text per viewport and how many fall outside the declared scale. -- **Unless:** Sizes must stay distinguishable, so large text may step many points apart; Marketing pages may add a display size -- **Also stated as:** canon.vignelli-two-type-sizes (Massimo Vignelli). - -## 30. `craft.button-hierarchy-one-primary` +## 26. `craft.button-hierarchy-one-primary` One filled brand-colour button per view; secondaries outlined, tertiaries as text, destructive actions quiet with a confirmation step. @@ -339,8 +297,9 @@ One filled brand-colour button per view; secondaries outlined, tertiaries as tex - **Don't:** Let a green button compete with the primary; Put a big red Delete beside a small Save; Colour every link brand blue - **Look at:** Classify buttons as filled, outlined or text from computed style; fail if a view or dialog has more than one filled button of distinct hues, or a delete-labelled button is filled while the confirming action is not. - **Unless:** Segmented or toggle groups; Toolbars of equal-weight actions; A page whose only job is the destructive action, where red is primary +- **Also stated as:** writing.destructive-actions-name-the-consequence (IBM Carbon Design System). -## 31. `craft.wcag-contrast-and-dont-rely-on-colour-alone` +## 27. `craft.wcag-contrast-and-dont-rely-on-colour-alone` Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. @@ -350,9 +309,9 @@ Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dar - **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only - **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. - **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself -- **Also stated as:** modern.contrast-and-not-colour-alone (Apple). +- **Also stated as:** modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group). -## 32. `craft.anything-but-dropdowns` +## 28. `craft.anything-but-dropdowns` Before a dropdown, try a switch, segmented control, radios, cards, typeahead, calendar, text input or stepper. @@ -362,9 +321,33 @@ Before a dropdown, try a switch, segmented control, radios, cards, typeahead, ca - **Don't:** Use a select for five or fewer options; Use three selects for a date; Ship a 195-country select without search on mobile - **Look at:** For each select: fail at 5 or fewer options unless committed as a rarely changed default; fail 2–3 adjacent selects with day, month, year options; flag over 30 options at 375px with no typeahead. - **Unless:** Users rarely need to change the default value; There are very few options; The user is not on mobile -- **Also stated as:** modern.boring-and-familiar-beats-novel (Scott Berkun). +- **Also stated as:** modern.boring-and-familiar-beats-novel (Scott Berkun); forms.memorable-dates-as-three-fields (GOV.UK Design System). + +## 29. `craft.tap-targets-and-control-height` + +Touch targets are at least 44×44; inputs and the buttons beside them share one height of 40 or 48px. + +- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html +- **In their words:** "The minimum tap target size on both iOS and Android — On iOS: 44x44pt. On Android: 48x48pt." +- **Do:** Give touch viewports hit areas of 44×44 or more; Match button height to the inputs beside it +- **Don't:** Use 26px-wide grid cells as targets; Make buttons shorter than the inputs they sit beside +- **Look at:** At 375px, getBoundingClientRect of every a, button, input and role=button: fail under 44 in width or height (under 24 for inline text links); in a form row, button and input heights within 2px. +- **Unless:** Inline text links in running prose; Dense desktop-only tools if the project commits to no touch +- **Also stated as:** modern.hit-targets-meet-platform-minimums (Apple). + +## 30. `craft.type-scale-few-font-sizes` -## 33. `craft.hierarchy-is-everything-squint-test` +Use about four font sizes from a fixed scale; reuse the default size for body, menus, lists and controls. + +- **Source:** Erik D. Kennedy, *The Responsive Website Font Size Guidelines* — https://www.learnui.design/blog/mobile-desktop-website-font-size-guidelines.html +- **In their words:** "Even the most interaction-heavy pages can typically look just fine with about 4 font sizes total." +- **Do:** Use header, default, secondary (default minus 2px) and one wildcard size; Reuse the default size across body, menus, lists and controls +- **Don't:** Invent a new size per component; Apply a strict modular or golden-ratio scale to responsive pages +- **Look at:** Count distinct computed font-size values on visible text per viewport and how many fall outside the declared scale. +- **Unless:** Sizes must stay distinguishable, so large text may step many points apart; Marketing pages may add a display size +- **Also stated as:** canon.vignelli-two-type-sizes (Massimo Vignelli). + +## 31. `craft.hierarchy-is-everything-squint-test` Squint: the most important thing must catch the eye first and the least important last; one element dominates each screen. @@ -375,7 +358,7 @@ Squint: the most important thing must catch the eye first and the least importan - **Look at:** Given a declared primary action, score every interactive element as area x contrast x font-weight factor; the declared primary must rank first, and no set of buttons may sit within 10% of each other. - **Unless:** Page titles are the only element styled all-out up-pop; A browsing page such as a gallery may have no single most important thing -## 34. `craft.greys-dont-have-to-be-grey-never-use-black` +## 32. `craft.greys-dont-have-to-be-grey-never-use-black` Tint the neutral scale toward the brand hue and never use pure black for text. @@ -386,7 +369,7 @@ Tint the neutral scale toward the brand hue and never use pure black for text. - **Look at:** Count text, border and background colours with saturation 0 and lightness under 15%, and the share of neutral swatches with saturation exactly 0. - **Unless:** It does not always work; worth a trial; High-contrast or accessibility modes and print -## 35. `craft.grids-are-overrated-content-dictates-width` +## 33. `craft.grids-are-overrated-content-dictates-width` Let content dictate its own width and the layout follow; do not stretch components because the screen grew. @@ -397,7 +380,7 @@ Let content dictate its own width and the layout follow; do not stretch componen - **Look at:** At 360–414px viewport, every content edge sits at 16px (or one consistent gutter) from the viewport edge and nothing overflows horizontally. - **Unless:** Strict grids for posters and websites where the artistry of the composition matters more than the information -## 36. `canon.bringhurst-leading-is-a-rhythmic-unit` +## 34. `canon.bringhurst-leading-is-a-rhythmic-unit` The leading is the vertical unit; add and remove vertical space in multiples of it. @@ -408,7 +391,7 @@ The leading is the vertical unit; add and remove vertical space in multiples of - **Look at:** Body line-height L; margins and paddings between text blocks as multiples of L (or L/2 if the lens allows). - **Unless:** More leading for longer measures, darker faces, larger x-height and sans serifs: the ratio moves with the face -## 37. `canon.butterick-emphasis-sparingly` +## 35. `canon.butterick-emphasis-sparingly` One emphasis device at a time: bold or italic, never both; underline only links; caps only under one line and tracked. @@ -419,7 +402,7 @@ One emphasis device at a time: bold or italic, never both; underline only links; - **Look at:** Elements with font-weight ≥600 and italic; underline on non-anchors; uppercase on blocks rendering more than one line; centred multi-line paragraphs; uppercase runs with letter-spacing under 0.05em. - **Unless:** Underline for web links; All caps under one line of text -## 38. `modern.defaults-are-decisions-you-inherited` +## 36. `modern.defaults-are-decisions-you-inherited` A library or AI default is someone else's decision; commit to one written aesthetic direction instead of inheriting it. @@ -431,6 +414,29 @@ A library or AI default is someone else's decision; commit to one written aesthe - **Unless:** A purple brand is allowed to be purple; the tell is the constellation and the absence of a decision, not any one colour - **Also stated as:** craft.supercharge-the-defaults (Steve Schoger (attendee notes by ynotdraw)). +## 37. `modern.readable-type-sizes-and-weights` + +Body text sits at the platform default size, weights stay 400 or heavier, and weight never changes on hover. + +- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ +- **In their words:** "Font weights below 400 should not be used" +- **Do:** 17px body on touch, 13px minimum on desktop UI; Weights between 400 and 700; Headings at weight 500–600; Minimise the number of typefaces +- **Don't:** Body text under 11pt; font-weight 300 or lower; Weight swaps on hover or selected state +- **Look at:** Computed font-size and font-weight of every text node at a phone viewport; count distinct font-family stacks and warn above 2. +- **Unless:** Captions and legal text may sit at the platform minimum; Display headings may use light weights at large sizes + +## 38. `modern.numbers-and-text-do-not-shift-layout` + +Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. + +- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ +- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" +- **Do:** tabular-nums in tables, timers and prices; width and height on every img +- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover +- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. +- **Unless:** Prose numbers +- **Also stated as:** data-display.right-align-numbers-tabular-figures (GitHub Primer). + ## 39. `modern.delight-scales-with-rarity` Spend delight on rare moments, keep daily actions plain, and never let an element visibly duplicate itself during a transition. @@ -484,13 +490,200 @@ Empty, sparse, dense, error and loading states are designed; skeletons match fin - **Don't:** Blank screens on empty arrays; Spinners that reflow content; Overflow from long strings - **Look at:** Render with [], with 1 item, with 500 items and with a 300-character title; assert no horizontal overflow, no overlap, an actionable control in the empty state, and skeleton-to-loaded CLS under a threshold. -## 44. `modern.numbers-and-text-do-not-shift-layout` +## 44. `color.status-colours-keep-their-meaning` -Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. +Each status colour has one meaning across the product (critical for errors and blocked actions, warning for what needs attention, success for what went well, info for tips) and is never borrowed for promotion or decoration. -- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ -- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" -- **Do:** tabular-nums in tables, timers and prices; width and height on every img -- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover -- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. -- **Unless:** Prose numbers +- **Source:** Shopify Polaris, *Colors: Palettes and roles (Critical, Success)* — https://polaris.shopify.com/design/colors/palettes-and-roles +- **In their words:** "Elements using critical must convey messaging that implies that an action is impossible, blocked, or has resulted in an error." +- **Do:** Map error, warning, success and info to named roles or tokens and use them only in those roles; Reserve the critical red for errors, blocked actions and destructive buttons; Use the info role, not warning or critical, for tips and announcements +- **Don't:** A sale or 'new' badge in the error red; Success green used to entice or to advertise an offer; Warning colour for 'coming soon' or 'under construction' messaging; Two different reds meaning error on different screens +- **Look at:** Find the colour the page uses for error text (an element with role=alert, aria-invalid's described-by message, or a class/token named error/critical/danger) and the success colour likewise. Count painted elements (text, fill or border) whose colour equals that error or success colour, within a ΔE of 3, and that are neither a validation message, an invalid field, a status badge of that meaning, nor a destructive action. +- **Unless:** Brand colours that happen to be red, as long as a separate, distinct error red is used for errors; Data visualisations where a series colour coincides with a status hue but no status is implied (prefer avoiding it) + +## 45. `color.from-tokens-not-hex` + +Every colour on the page comes from the design system's named tokens or palette functions, never from hex values copied into components. + +- **Source:** GOV.UK Design System (Government Digital Service), *Styles: Colour* — https://design-system.service.gov.uk/styles/colour/ +- **In their words:** "Do not copy the specific hexadecimal (hex) colour values." +- **Do:** Reference colour by role token (brand, text, error, border) rather than by value; Use palette colours (tints and shades of a few families) for supporting elements; Use a functional token only in the context it is designed for +- **Don't:** Hex literals in component styles; Near-duplicate colours (#1d70b8 next to #1d70b9) created by eye-dropping; Using the error token as a general red +- **Look at:** Collect every computed color, background-color, border-*-color, outline-color and fill/stroke of painted elements, and every value of CSS custom properties declared on :root (and on any theme selector). Count distinct painted colours that match no custom-property value (exact RGBA after resolution), and count pairs of painted colours closer than ΔE 2 that are not identical. +- **Unless:** Images, illustrations and embedded third-party widgets; Browser defaults on unstyled native controls; GOV.UK: palette colours (not functional ones) are allowed for illustrations and custom components + +## 46. `color.one-action-colour-apart-from-status` + +Links and primary actions share one interactive colour family, used consistently, and that colour is not the colour of errors, warnings or success. + +- **Source:** IBM Carbon Design System, *Elements: Color, Overview (Color anatomy)* — https://carbondesignsystem.com/elements/color/overview/ +- **In their words:** "The core blue family serves as the primary action color across all IBM products and experiences. Additional colors are used sparingly and purposefully." +- **Do:** One link colour, used only for links; Primary buttons in the brand or action colour; Danger colour reserved for destructive buttons, not for the primary action +- **Don't:** Links in the error red; Primary buttons in several different hues across screens; Static text coloured like links +- **Look at:** Take the computed text colour of every a[href] in running text and the background of every primary (first, filled, or type=submit) button. Count distinct hues among them (30° bins), and count links or primary buttons whose colour is within ΔE 10 of the page's error, warning or success colour. Also count non-interactive text elements painted in the link colour. +- **Unless:** Primer (GitHub) deliberately uses the success role for primary buttons; a system that states such a mapping consistently is following its own rule; Destructive primary actions (Delete account) take the danger colour on purpose; Navigation menus, where position signals the link + +## 47. `color.controls-and-graphics-3-to-1` + +The parts that show a control is there and what state it is in (input borders, checkbox boxes and ticks, toggle tracks, icon-only buttons, meaningful chart marks) contrast at least 3:1 with the colours next to them. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "Unless the control is inactive, any visual information provided that is necessary for a user to identify that a control is present and how to operate it must have a minimum 3:1 contrast ratio with the adjacent colors." +- **Do:** Input borders at 3:1 against the background the input sits on, or a filled input background at 3:1; Checkbox ticks and radio dots at 3:1 against the box; Standalone icons at 3:1 against their background; Avoid very thin lines that anti-alias below the nominal ratio +- **Don't:** Pale grey input borders (#ddd on white is about 1.4:1); A selected state shown only by a faint tint; Hover effects that lower a control's contrast with its surroundings +- **Look at:** For each visible input, select, textarea, checkbox, radio, [role=switch] and icon-only button (no visible text): compute the contrast ratio between the colour that identifies it (border colour, or its own background when it has no border, or the icon fill) and the background behind it; count those below 3:1, unrounded. Disabled controls are skipped. +- **Unless:** Inactive (disabled) controls are exempt; A control identified by its visible text needs no contrasting boundary; Logos and decorative graphics; Appearance determined by the browser and not modified by the author + +## 48. `color.focus-ring-contrasts-with-its-surroundings` + +The focus indicator contrasts at least 3:1 with whatever it is drawn against: the page background for an outer ring, the component's own colours for an inner one. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast, Relationship with Focus Visible (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." +- **Do:** An outer ring that contrasts with the page background; A two-colour ring (dark and light) that holds on any background; A thick indicator rather than a 1px one +- **Don't:** A yellow outer ring on a white page; A focus border that changes hue inside the component without contrasting with its fill; Focus shown only by a background tint change +- **Look at:** Tab to each focusable control; diff the focused and unfocused screenshots of the control's box padded by a few pixels; for the changed pixels, take their colour and the colour of the unchanged pixels adjacent to them (page background outside, component fill inside). Count controls where no changed region reaches 3:1 against its adjacent colour. page.focus-visible checks only that some pixel changes; this checks that the change can be seen. +- **Unless:** Unmodified browser default focus styles; WCAG does not compare focused and unfocused states with each other; a background-only change is out of scope for 1.4.11 but fails Use of Color + +## 49. `color.few-families-in-proportion` + +Use a few colour families in a deliberate proportion (neutral base dominant, then primary, secondary and a small accent), not an even spread of many hues. + +- **Source:** U.S. Web Design System (GSA), *Design tokens: Theme color tokens* — https://designsystem.digital.gov/design-tokens/color/theme-tokens/ +- **In their words:** "about 60% of your site’s color would be the primary color family, about 30% would be the secondary color family, and about 10% would be the accent color families" +- **Do:** Neutral base for text and most surfaces; One primary family carrying most of the colour, one secondary, a small accent; Additional colours used sparingly and for a purpose (Carbon); Start in black and white, then add colour to support the message (USWDS) +- **Don't:** Five or more saturated hue families at similar weight on one screen; An accent that covers more area than the primary; A new hue introduced for a single component +- **Look at:** Screenshot the page and bucket every non-neutral pixel (HSL saturation above about 15%) by hue into 30° bins, ignoring images and status colours. How many hue families take more than 2% of the coloured area, and does the largest one carry most of it while the smallest (accent) stays near a tenth? +- **Unless:** USWDS: the proportions are for non-base colours; neutral text will usually dominate; Illustration, photography and data visualisation, which need their own palettes; Brands whose identity is multi-hue + +## 50. `writing.success-names-what-happened` + +A success message names the thing and what happened to it, in a few words ('Product updated'), instead of a generic 'Success!' or a wordy 'Your product has been successfully updated'. + +- **Source:** Shopify Polaris, *Toast component, Content guidelines: Message* — https://polaris.shopify.com/components/internal-only/toast +- **In their words:** "Written in the pattern of: noun + verb" +- **Do:** Object + past-tense verb: 'Collection added', 'Invoice sent'; Include the identifier when there are several (e.g. 'Issue #21 created' with a link); On the final page of a transaction, add a reference and what happens next +- **Don't:** A bare 'Success!', 'Done' or 'Saved successfully' with no object; 'Your X has been successfully Y-ed' padding; Title Case or field-colon formats like 'Discount: Saved successfully' +- **Look at:** Drive each save/create/send/delete action and capture the resulting status text (role=status, aria-live regions, toasts, banners). Count messages that contain no noun from the action's object (the button label's object or the page's entity name), or that are only a generic word {success, done, saved, ok, complete}, or that exceed 8 words for a toast. Count must be 0. +- **Unless:** When the result is already obvious on screen (the edited item reappears updated, or the user lands on the new item), Primer says no extra success message is needed — screen readers still need it announced; Confirmation pages at the end of a transaction carry more: reference number and what happens next + +## 51. `writing.buttons-name-the-action` + +A button's label is a verb (plus its object when needed) that says what happens when it is pressed; vague labels like OK, Done, Submit or Yes are replaced whenever a specific action exists. + +- **Source:** IBM Carbon Design System, *Dialog pattern, anatomy: Actions* — https://carbondesignsystem.com/patterns/dialog-pattern/ +- **In their words:** "Use descriptive words for the actions like Add, Delete, Save and avoid vague words like Done or OK." +- **Do:** Start the label with a verb: 'Save and continue', 'Send invoice', 'Add product'; Name the object when the verb alone is ambiguous on that screen; Make the dialog title and its confirming button use the same verb +- **Don't:** 'OK', 'Done', 'Submit', 'Yes', 'Go', 'Confirm' where a specific action exists; Labels that describe the button rather than the action ('Button', 'Click'); Icon-only buttons without an accessible name +- **Look at:** List every visible button and input[type=submit] with its accessible name. Count those whose trimmed, lower-cased name is in the generic set {ok, okay, done, submit, yes, go, confirm, click here, click, button} or is empty. Count must be 0 outside of a platform-standard alert with a single acknowledgement. +- **Unless:** A purely informational alert with one acknowledgement button may say 'OK'; GOV.UK uses 'Continue' for a question page that does not save the user's answers — a convention for multi-page forms, not a generic label + +## 52. `writing.one-label-per-action` + +One concept, one word: controls that do the same thing carry the same label everywhere, and controls that do different things never share a label. + +- **Source:** GitHub Primer, *Accessibility guide: Descriptive buttons, 'How to test names'* — https://primer.style/guides/accessibility/descriptive-buttons +- **In their words:** "When buttons perform the same action, they have the same name." +- **Do:** Keep a terminology list of preferred words and words not to use for the product; Pick one verb per action (Delete or Remove, not both for the same thing) and reuse it across pages; Add the object to disambiguate repeated actions ('Remove Apples', 'Remove Pears') +- **Don't:** Synonyms for one action across screens: 'Save' here, 'Update' there, 'Apply' elsewhere; Identical labels for different actions on the same page; Naming the same object two ways ('workspace' and 'project') in one product +- **Look at:** Across the journey's pages, collect (accessible name, action) pairs for buttons and links, where action is the form action/href/handler target. Count names that map to two or more different actions on one page, and actions reached by two or more different names across pages; also flag known synonym pairs present together (save/update/apply, delete/remove, sign in/log in, cart/basket). Count must be 0. +- **Unless:** Delete and Remove may coexist when they mean different things (destroy vs take out of a collection), as Carbon defines them — then each must be used only for its own meaning + +## 53. `writing.dates-numbers-units-for-the-reader` + +Dates spell out the month, numbers are numerals with thousands separators, and units sit a space after their number — formatted in the reader's locale, never as an ambiguous all-numeric date or a raw machine value. + +- **Source:** Shopify Polaris, *Content: Grammar and mechanics, 'Numbers, dates, and currency'* — https://polaris.shopify.com/content/grammar-and-mechanics +- **In their words:** "Use the month’s full name. If there isn’t enough space, use 3-letter abbreviations. Don’t write dates with numerals only." +- **Do:** 'December 11, 2024' or 'Dec 11, 2024' (in the reader's locale order); Numerals, not words: 'You have 5 orders to fulfill'; Thousands separators: '12,000'; A space between number and unit: '3.4 lb', '2 kg'; Currency code after the amount when currencies can be confused: '$10,000 USD'; Format with Intl.DateTimeFormat / Intl.NumberFormat for the user's locale +- **Don't:** All-numeric dates like '12/11/24'; ISO timestamps or epoch values shown raw ('2024-12-11T09:30:00Z'); Ordinals in dates ('January 23rd'); Unit glued to the number ('3.4lb'); Shortened numbers like '12 k' where the exact value matters +- **Look at:** Scan visible text nodes. Count matches of all-numeric dates (\b\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}\b), raw ISO timestamps (\d{4}-\d{2}-\d{2}T\d{2}:), integers of 5+ digits with no separator outside codes/IDs, and numbers glued to a unit (\d(kg|lb|cm|mm|km|mi|ml|oz)\b). Count must be 0. +- **Unless:** Polaris notes these are American English base rules and dates, numbers and measurements should be localized automatically — the target is the reader's locale, not US format; Identifiers, codes, SKUs and years are not quantities and take no separator; Dense data tables may use compact numeric dates if the format is unambiguous for the locale and stated in the column header + +## 54. `data-display.text-left-headers-follow-their-column` + +Text cells are left-aligned, nothing in a data table is centred, and each column header takes the same alignment as the data below it. + +- **Source:** W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.), *Tables Tutorial — Tips and Tricks: Alignment* — https://www.w3.org/WAI/tutorials/tables/tips/ +- **In their words:** "Align text to the left and numeric data to the right (in left-to-right languages), so that people using larger text sizes or smaller screens will be able to find it. … It’s helpful to give column headers the same alignment as the data in the cells below." +- **Do:** Left-align (start-align) textual cells and their headers; Give each th the same horizontal alignment as the cells of its column; Mirror alignment in right-to-left languages +- **Don't:** Centre-aligned text or number columns; A left-aligned header above a right-aligned numeric column; Centred headers as a table-wide default +- **Look at:** For each table column: count th whose computed text-align differs from the dominant text-align of the td in the same column, and count td/th whose computed text-align is center (excluding cells that hold only a checkbox, icon or status badge). +- **Unless:** A column holding only a checkbox, icon or single glyph may be centred; Right-to-left scripts reverse the sides + +## 55. `data-display.no-tables-for-layout` + +A table is for comparing data in rows and columns, never for arranging content on the page; layout belongs to the grid. + +- **Source:** GOV.UK Design System (Government Digital Service), *Table — When not to use this component* — https://design-system.service.gov.uk/components/table/ +- **In their words:** "Never use the table component to layout content on a page." +- **Do:** CSS grid or flex for page and dashboard layout; A list, cards or a summary list for items that do not share columns; Tables only where every row has the same fields +- **Don't:** A table that positions a sidebar, form or dashboard tiles; Table cells holding headings, paragraphs or whole forms; role=presentation on a table that actually holds tabular data +- **Look at:** Count table elements that have no th and either contain headings, more than one paragraph per cell, form fieldsets or nested tables, or have a single row whose cells hold unrelated content blocks. +- **Unless:** HTML email, where tables are still the only reliable layout tool + +## 56. `data-display.summary-list-for-key-value-facts` + +A set of facts about one thing — label and value pairs — is shown as a summary list (dl with dt and dd), not as a table with no headers or as loose text. + +- **Source:** GOV.UK Design System (Government Digital Service), *Summary list — When to use / When not to use this component* — https://design-system.service.gov.uk/components/summary-list/ +- **In their words:** "Use a summary list to show information as a list of key facts. … only use it to present information that has a key and at least one value." +- **Do:** dl with dt (the key) and dd (the value) for record details, metadata and check-your-answers pages; A row action ('Change') whose accessible name includes the key ('Change name'); Headings or cards to separate several summary lists on one page +- **Don't:** A two-column table without th used to show one record's fields; Key–value pairs set as 'Label: value' runs in a paragraph; A summary list for genuinely tabular data or a plain list of items +- **Look at:** Where the screen shows the fields of a single record (a profile, an order, an item's metadata, a check-answers page), are the label–value pairs marked up as dl/dt/dd with each key visually distinct from its value — and is tabular data comparing several records in a table rather than a summary list? +- **Unless:** Two or three facts inside a card may be inline text if they are not scanned as a set + +## 57. `forms.group-related-inputs-in-a-fieldset` + +Wrap each set of inputs that answer one question — radios, checkboxes, a date, an address — in a fieldset whose first child is a legend naming the question. + +- **Source:** U.S. Web Design System (USWDS), *Form component — Accessibility guidance* — https://designsystem.digital.gov/components/form/ +- **In their words:** "Group each set of thematically related controls in a fieldset element. Use the legend element to offer a label within each one." +- **Do:** fieldset + legend for every radio group and checkbox group; fieldset + legend for date (day/month/year) and multi-line address; The legend as the page h1 when the page asks only that question; '(optional)' in the legend when the whole group is optional +- **Don't:** A radio group whose question is a <p> or <div> above the radios; Radio and checkbox groups with no group name for assistive tech; A fieldset with no legend or an empty one +- **Look at:** Count radio groups (same name), checkbox groups, and day/month/year triples. How many are not inside a fieldset whose first element child is a non-empty legend (or a role=group / radiogroup with aria-labelledby pointing at visible text)? +- **Unless:** A single checkbox (I agree) needs no fieldset; Native fieldset/legend can be replaced by role=group with aria-labelledby where styling requires it + +## 58. `forms.do-not-disable-the-submit-button` + +Keep the submit button enabled while the form is incomplete; let a press show what is missing instead of greying the button out. + +- **Source:** GitHub Primer, *Forms UI pattern — Validation* — https://primer.style/product/ui-patterns/forms/overview/ +- **In their words:** "Disabled buttons are discouraged, as they don't clearly communicate what actions a user should take to complete a form." +- **Do:** An always-enabled submit that runs validation and shows the error summary; aria-disabled with an explanation where a control truly must be unavailable; Ignoring a second click while a submit is in flight, with a visible busy state +- **Don't:** A grey, unfocusable submit until every field validates; A disabled button with no text saying why; Disabled text inputs used as read-only display +- **Look at:** Load the form untouched. Is the primary submit button disabled (disabled attribute, aria-disabled=true, or pointer-events:none)? If enabled, does pressing it produce visible field errors within 1 s? +- **Unless:** Briefly blocking a second submit while the first is processing (USWDS, GOV.UK prevent-double-click); GOV.UK: only when research shows a disabled button makes the interface easier to understand + +## 59. `forms.validate-when-the-user-is-done` + +Do not show an error while the user is still typing; validate when they try to continue, and only add earlier validation where research shows it helps. + +- **Source:** GOV.UK Design System, *Recover from validation errors pattern — When to tell the user about validation errors* — https://design-system.service.gov.uk/patterns/validation/ +- **In their words:** "Generally speaking, avoid validating the information in a field before the user has finished entering it. This sort of validation can cause problems - especially for users who type more slowly." +- **Do:** Validate on Continue or Submit; After a failed submit, update a field's error live once the user fixes it (Primer); A character count is the accepted exception: warn as the limit is passed +- **Don't:** An error that appears on the first keystroke of an email or phone field; Red borders on an untouched form at load; Browser-native HTML5 validation bubbles in place of designed messages +- **Look at:** Focus an email or formatted field and type one character, keeping focus. Wait 1 s. Does any error message, aria-invalid=true or error colour appear before blur or submit? Also load the form fresh: are any fields already marked invalid? +- **Unless:** GOV.UK goes further and says not to validate on blur either; Baymard (usability.inline-validation-after-leaving-field) recommends blur validation for hard fields — the schools agree only on 'not while typing'; Primer allows validating as the user types once the field has already been flagged invalid, so the error clears as soon as it is fixed + +## 60. `forms.keep-answers-after-an-error` + +When a submit fails, show the form again with every answer the user gave still in it — the failing ones and the passing ones. + +- **Source:** GOV.UK Design System, *Error message component — How it works* — https://design-system.service.gov.uk/components/error-message/ +- **In their words:** "Do not clear any form fields when showing the Error message component. Keep both passing and failing answers." +- **Do:** Re-render server-side errors with submitted values filled in; Keep the failing value so the user can see and edit what went wrong; Pre-populate fields when the user goes back to change an answer +- **Don't:** A reload that empties the form after a server error; Clearing the field that failed; Clearing password-adjacent fields such as name and email along with the password +- **Look at:** Fill every field with valid values except one, submit, and after the error page paints read each input's value: how many fields that had a value now read empty? +- **Unless:** Password and card security code fields may be cleared for security, and the message should say so + +## 61. `forms.autocomplete-names-the-purpose` + +Every field that asks about the user — name, email, phone, address, postcode, birthday, card — carries the matching autocomplete token so browsers and assistive tech can fill and label it. + +- **Source:** W3C Web Accessibility Initiative, *Understanding WCAG 2.2 Success Criterion 1.3.5: Identify Input Purpose* — https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html +- **In their words:** "Use code to indicate the purpose of common inputs, where technology allows." +- **Do:** autocomplete='name' or 'given-name' and 'family-name' on name fields; 'email', 'tel', 'postal-code', 'street-address' or 'address-line1', 'bday-day'/'bday-month'/'bday-year'; 'shipping' and 'billing' section tokens on order forms; autocomplete='off' on the form, if needed, while each field still declares its purpose +- **Don't:** autocomplete='off' on personal fields with no purpose token; Personal-data fields with no autocomplete attribute; A token that does not match the field (email token on a phone field) +- **Look at:** For every input, select and textarea whose label matches name, email, phone, address, postcode/ZIP, city, country, date of birth, card number or expiry: count those whose autocomplete attribute is missing, 'off', or not a WCAG input-purpose token that matches the label. +- **Unless:** Fields about someone other than the user (a recipient's email) are outside 1.3.5; A field that accepts either username or email may carry one token or none diff --git a/.claude/skills/uxcli/lenses/workspace.md b/.claude/skills/uxcli/lenses/workspace.md index d5effbb..daf64f8 100644 --- a/.claude/skills/uxcli/lenses/workspace.md +++ b/.claude/skills/uxcli/lenses/workspace.md @@ -2,9 +2,33 @@ Admin, CRUD, tables, settings and B2B tools: repeated work where speed and consistency matter. -45 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. +63 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. -## 1. `usability.feedback-within-a-second` +## 1. `usability.visibility-of-system-status` + +Every action with consequences shows the user something changed, as quickly as possible, so they always know what the system is doing. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #1* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "The design should always keep users informed about what is going on, through appropriate feedback within a reasonable amount of time." +- **Do:** Show a visible trace of every state change; Give feedback immediately, or as quickly as possible; Disable and label a control while its action runs +- **Don't:** Submit silently; Change state with no visible trace; Show an action's result only somewhere else +- **Look at:** Drive an action, then diff DOM or pixels over the next frames: did anything visibly change within 1 s; is there a live region, status text, spinner or busy state? +- **Unless:** Below 0.1 s no special feedback is needed beyond showing the result +- **Also stated as:** modern.feedback-is-local-and-optimistic (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn); writing.success-names-what-happened (Shopify Polaris); data-display.loading-skeleton-holds-the-layout (GitHub Primer); forms.do-not-disable-the-submit-button (GitHub Primer). + +## 2. `usability.speak-the-users-language` + +Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #2* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "The design should speak the users' language. Use words, phrases, and concepts familiar to the user, rather than internal jargon." +- **Do:** Use words the user understands without looking them up; Spell acronyms out in full on each page; Write 'for example', not 'eg' or 'ie' +- **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations +- **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. +- **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se +- **Also stated as:** writing.errors-say-what-and-how-to-fix (GOV.UK Design System (Government Digital Service)); writing.plain-language-reading-level (Shopify Polaris); forms.error-message-says-how-to-fix (GOV.UK Design System); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen). + +## 3. `usability.feedback-within-a-second` Paint something within 0.1 s; keep the user's flow with a response under 1 s; show a progress indicator for anything longer. @@ -14,9 +38,9 @@ Paint something within 0.1 s; keep the user's flow with a response under 1 s; sh - **Don't:** Leave dead time with no indicator; Show a percent-done bar that lies badly - **Look at:** Time from input event to first paint of any change; presence of a progress element, aria-busy, <progress> or role=status update when the wait exceeds 1 s. - **Unless:** Laws of UX claims a purposeful delay can raise perceived value — an opinion with no study cited -- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn). +- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg). -## 2. `usability.signifiers-make-clickable-look-clickable` +## 4. `usability.signifiers-make-clickable-look-clickable` Links and buttons must look clickable — colour, underline, border or fill — because weak signifiers cost measured time and fixations. @@ -28,7 +52,19 @@ Links and buttons must look clickable — colour, underline, border or fill — - **Unless:** Link position in nav menus or peripheral lists may eliminate the need for underlining (Loranger); Flat works best with low information density, traditional layouts and high-contrast targets positioned standardly (Moran) - **Also stated as:** modern.signifiers-survive-flatness (Kate Moran (NN/G)); modern.no-dead-zones-and-honest-clickability (Rauno Freiberg). -## 3. `usability.fitts-target-size-and-distance` +## 5. `usability.omit-needless-words` + +Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. + +- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) +- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" +- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns +- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case +- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. +- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text +- **Also stated as:** writing.sentence-case-ui-text (IBM Carbon Design System). + +## 6. `usability.fitts-target-size-and-distance` Make targets big, space them apart, and put them where the pointer already is; touch targets at least 1 cm square. @@ -40,19 +76,19 @@ Make targets big, space them apart, and put them where the pointer already is; t - **Unless:** Infinite targets along screen edges for mouse — size matters less at an edge; Primary CTAs, moving users, children and the elderly need larger than the minimum - **Also stated as:** craft.tap-targets-and-control-height (Erik D. Kennedy); modern.hit-targets-meet-platform-minimums (Apple). -## 4. `usability.visibility-of-system-status` +## 7. `usability.von-restorff-one-emphasis` -Every action with consequences shows the user something changed, as quickly as possible, so they always know what the system is doing. +One visually distinct primary action per view, left-aligned with the form, and distinguished by more than colour. -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #1* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "The design should always keep users informed about what is going on, through appropriate feedback within a reasonable amount of time." -- **Do:** Show a visible trace of every state change; Give feedback immediately, or as quickly as possible; Disable and label a control while its action runs -- **Don't:** Submit silently; Change state with no visible trace; Show an action's result only somewhere else -- **Look at:** Drive an action, then diff DOM or pixels over the next frames: did anything visibly change within 1 s; is there a live region, status text, spinner or busy state? -- **Unless:** Below 0.1 s no special feedback is needed beyond showing the result -- **Also stated as:** modern.feedback-is-local-and-optimistic (Rauno Freiberg). +- **Source:** GOV.UK Design System, *Button component* — https://design-system.service.gov.uk/components/button/ +- **In their words:** "Avoid using multiple default buttons on a single page. Having more than one main call to action reduces their impact, and makes it harder for users to know what to do next." +- **Do:** Make key actions visually distinctive; Use restraint so emphases do not compete; Align the primary button to the left edge of the form; Highlight the default, except for dangerous actions +- **Don't:** Put two or more filled primary buttons in one view; Signal emphasis by colour alone +- **Look at:** Cluster interactive elements by computed background, border and weight; the most emphatic cluster per view must have one member; primary's left edge aligns with the inputs; emphasis differs in weight, fill or border, not colour only. +- **Unless:** Do not pre-highlight a dangerous action as the default (Nielsen) +- **Also stated as:** craft.button-hierarchy-one-primary (Steve Schoger); writing.destructive-actions-name-the-consequence (IBM Carbon Design System). -## 5. `usability.natural-mapping-and-proximity` +## 8. `usability.natural-mapping-and-proximity` Put a control next to the thing it changes and a label next to its field; gaps between groups must exceed gaps within them. @@ -64,7 +100,7 @@ Put a control next to the thing it changes and a label next to its field; gaps b - **Unless:** Left-aligned labels are acceptable if space is constrained and labels are of similar length - **Also stated as:** canon.gestalt-proximity (Aurora Harley, NN/g). -## 6. `usability.follow-conventions` +## 9. `usability.follow-conventions` Work the way the sites and platforms users already know; a convention beats a locally optimised novelty. @@ -76,7 +112,19 @@ Work the way the sites and platforms users already know; a convention beats a lo - **Unless:** Desktop apps follow their own OS: Windows OK-first, Apple OK-last - **Also stated as:** craft.anything-but-dropdowns (Erik D. Kennedy); modern.boring-and-familiar-beats-novel (Scott Berkun). -## 7. `usability.inline-validation-after-leaving-field` +## 10. `usability.help-in-context` + +The best help is none; when a field needs explaining, put a short hint beside it at the moment it is needed, not on another page. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #10* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "Whenever possible, present the documentation in context right at the moment that the user requires it." +- **Do:** Use hint text for help relevant to most users; Keep hint text to one short sentence without full stops; Associate the hint with its input via aria-describedby +- **Don't:** Put help only on a separate page; Put links inside hint text; Write lengthy explanations +- **Look at:** For inputs with a format expectation (date, postcode, card): a hint associated via aria-describedby and rendered adjacent, same column, within one line-height; hint length in sentences. +- **Unless:** Nielsen's own first sentence: it is best if the system needs no additional explanation +- **Also stated as:** usability.placeholders-are-not-labels (Katie Sherwin, NN/g); forms.hint-text-is-short-and-linked (GOV.UK Design System). + +## 11. `usability.inline-validation-after-leaving-field` Validate a hard field after the user leaves it, never on focus or while typing, and clear the error live once it is fixed. @@ -87,18 +135,18 @@ Validate a hard field after the user leaves it, never on focus or while typing, - **Look at:** Drive a field: focus shows no error; typing an invalid value shows none (unless length threshold reached); blur shows an error adjacent via aria-describedby; typing a fix clears it without blur. - **Unless:** Simple fields need none; premature validation was worse than none (Wroblewski) -## 8. `usability.speak-the-users-language` +## 12. `usability.clearly-marked-emergency-exit` -Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. +Every interaction has a visible, labelled way out — Cancel, Back, Escape, Undo — so a mistaken action does not trap the user. -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #2* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "The design should speak the users' language. Use words, phrases, and concepts familiar to the user, rather than internal jargon." -- **Do:** Use words the user understands without looking them up; Spell acronyms out in full on each page; Write 'for example', not 'eg' or 'ie' -- **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations -- **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. -- **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #3* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "Users often perform actions by mistake. They need a clearly marked 'emergency exit' to leave the unwanted action without having to go through an extended process." +- **Do:** Support Undo and Redo; Show a clear Cancel or close control; Label the exit clearly and make it discoverable +- **Don't:** Open a modal with no close; Build a wizard step with no back; Ship a destructive action with no undo +- **Look at:** For each role=dialog or modal: a focusable control whose text or aria-label matches close/cancel/back, and Escape dismisses it; for each step past the first in a flow: a back control exists. +- **Unless:** Legally required interstitials — the exit must still be visible, not necessarily free of consequence -## 9. `usability.no-false-affordances` +## 13. `usability.no-false-affordances` Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. @@ -108,30 +156,7 @@ Nothing that is not interactive may look interactive: no underlined or blue stat - **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons - **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). -## 10. `usability.von-restorff-one-emphasis` - -One visually distinct primary action per view, left-aligned with the form, and distinguished by more than colour. - -- **Source:** GOV.UK Design System, *Button component* — https://design-system.service.gov.uk/components/button/ -- **In their words:** "Avoid using multiple default buttons on a single page. Having more than one main call to action reduces their impact, and makes it harder for users to know what to do next." -- **Do:** Make key actions visually distinctive; Use restraint so emphases do not compete; Align the primary button to the left edge of the form; Highlight the default, except for dangerous actions -- **Don't:** Put two or more filled primary buttons in one view; Signal emphasis by colour alone -- **Look at:** Cluster interactive elements by computed background, border and weight; the most emphatic cluster per view must have one member; primary's left edge aligns with the inputs; emphasis differs in weight, fill or border, not colour only. -- **Unless:** Do not pre-highlight a dangerous action as the default (Nielsen) -- **Also stated as:** craft.button-hierarchy-one-primary (Steve Schoger). - -## 11. `usability.omit-needless-words` - -Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. - -- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) -- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" -- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns -- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case -- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. -- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text - -## 12. `usability.aesthetic-usability-effect-bias` +## 14. `usability.aesthetic-usability-effect-bias` Polish makes a screen look more usable than it is; discount your aesthetic impression and run the measurable checks first. @@ -142,28 +167,6 @@ Polish makes a screen look more usable than it is; discount your aesthetic impre - **Look at:** Did the evaluator judge the screen usable because it looks good? Re-check the verdict against the count-kind entries before trusting it; 'attractive' is the variable and stays taste. - **Unless:** With severe usability issues, or functionality sacrificed for aesthetics, users lose patience -## 13. `usability.clearly-marked-emergency-exit` - -Every interaction has a visible, labelled way out — Cancel, Back, Escape, Undo — so a mistaken action does not trap the user. - -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #3* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "Users often perform actions by mistake. They need a clearly marked 'emergency exit' to leave the unwanted action without having to go through an extended process." -- **Do:** Support Undo and Redo; Show a clear Cancel or close control; Label the exit clearly and make it discoverable -- **Don't:** Open a modal with no close; Build a wizard step with no back; Ship a destructive action with no undo -- **Look at:** For each role=dialog or modal: a focusable control whose text or aria-label matches close/cancel/back, and Escape dismisses it; for each step past the first in a flow: a back control exists. -- **Unless:** Legally required interstitials — the exit must still be visible, not necessarily free of consequence - -## 14. `usability.help-in-context` - -The best help is none; when a field needs explaining, put a short hint beside it at the moment it is needed, not on another page. - -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #10* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "Whenever possible, present the documentation in context right at the moment that the user requires it." -- **Do:** Use hint text for help relevant to most users; Keep hint text to one short sentence without full stops; Associate the hint with its input via aria-describedby -- **Don't:** Put help only on a separate page; Put links inside hint text; Write lengthy explanations -- **Look at:** For inputs with a format expectation (date, postcode, card): a hint associated via aria-describedby and rendered adjacent, same column, within one line-height; hint length in sentences. -- **Unless:** Nielsen's own first sentence: it is best if the system needs no additional explanation - ## 15. `usability.postel-tolerant-input` Accept every reasonable form of an input — spaces, dashes, case, whitespace — and normalise it, instead of rejecting it. @@ -175,18 +178,7 @@ Accept every reasonable form of an input — spaces, dashes, case, whitespace - **Look at:** Drive each formatted field with equivalent variants ('4111 1111 1111 1111' vs '4111111111111111', ' a@b.co ', '+44 20…' vs '02…') and compare validation outcomes. - **Unless:** Inputs where ambiguity is dangerous, such as dates in medical or legal contexts -## 16. `usability.placeholders-are-not-labels` - -Every input has a visible label outside the field; placeholder text is never the label, the hint or the only format example. - -- **Source:** Katie Sherwin, NN/g, *Placeholders in Form Fields Are Harmful (2014)* — https://www.nngroup.com/articles/form-design-placeholders/ -- **In their words:** "Disappearing placeholder text strains users' short-term memory." -- **Do:** Give every text input a visible label; Put hints outside empty form fields -- **Don't:** Use placeholder text in place of a label; Use placeholder text for hints or examples; Hide the label visually and rely on the placeholder -- **Look at:** Any input or textarea with a placeholder and no visible associated label, or with a visually hidden label; placeholder used as the only format example. -- **Unless:** A single-field search where the button label carries the meaning is the usual carve-out; the fetched page states none explicitly - -## 17. `usability.labels-above-fields` +## 16. `usability.labels-above-fields` Stack the label above its field, left edges aligned, a few pixels apart, so label and field are read in one fixation. @@ -197,7 +189,7 @@ Stack the label above its field, left edges aligned, a few pixels apart, so labe - **Look at:** For each label/input pair: label box bottom ≤ input box top and label left edge ≈ input left edge within a few px; if side by side, the gap between label right edge and input left edge. - **Unless:** Landscape phones: switch to left-aligned to keep the field visible above the keyboard (Baymard); Left-aligned is acceptable when labels are of similar length and as close to the fields as possible (NN/g) -## 18. `usability.field-width-matches-expected-input` +## 17. `usability.field-width-matches-expected-input` Size each input to the data it expects: short boxes for year, postcode and CVV; one consistent width for variable data like email. @@ -208,7 +200,7 @@ Size each input to the data it expects: short boxes for year, postcode and CVV; - **Look at:** Rendered input width in ch of its font versus maxlength, inputmode, autocomplete token or pattern length: cc-csc wider than ~8 ch, postal-code at full width, or email narrower than ~20 ch. - **Unless:** Full-width inputs on narrow phones are the norm — check at desktop widths or compare relative widths within the form -## 19. `usability.start-with-user-needs-design-with-data` +## 18. `usability.start-with-user-needs-design-with-data` Name the actor and their need before drawing a screen, then let measured behaviour, not hunches or looks, decide what changes. @@ -218,7 +210,7 @@ Name the actor and their need before drawing a screen, then let measured behavio - **Don't:** Design to a hunch; Design to an aesthetic - **Look at:** Does the journey name its actor and need (uxcli context show), and is the design decision traceable to observed behaviour rather than a hunch or a look? -## 20. `usability.constraints-shift-complexity-to-the-system` +## 19. `usability.constraints-shift-complexity-to-the-system` Complexity that cannot be removed goes to the system, not the user: default what can be inferred, hide what is rarely needed. @@ -229,7 +221,7 @@ Complexity that cannot be removed goes to the system, not the user: default what - **Look at:** Count visible required inputs the page could default (no 'same as shipping' control, both address blocks open); presence of autocomplete tokens on address and payment inputs. - **Unless:** Tognazzini's counter-view: users resist complexity reduction and attempt harder tasks when systems get simpler -## 21. `craft.fewer-borders` +## 20. `craft.fewer-borders` Separate elements with space, a background shift, a shadow or striping before reaching for a border; too many borders make a design busy. @@ -239,7 +231,19 @@ Separate elements with space, a background shift, a shadow or striping before re - **Don't:** Use a 1px border as the default grouping device; Wrap a nav in a panel that only needs to recede; Rule every table row with a border when striping would do - **Look at:** For every text node, count ancestors with a visible border, non-none box-shadow or outline; report max nesting depth and bordered boxes per viewport. Counted by `page.nesting`: `review check` refuses a `holds` it contradicts. - **Unless:** Form inputs and the one object a person acts on keep their border; Keylines that make disconnected content feel connected; Decorative borders as style once the count is low -- **Also stated as:** canon.gestalt-common-region (Aurora Harley, NN/g); canon.rams-as-little-design-as-possible (Dieter Rams); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); canon.tufte-smallest-effective-difference (Edward Tufte); canon.tufte-one-plus-one-equals-three (Edward Tufte). +- **Also stated as:** canon.gestalt-common-region (Aurora Harley, NN/g); forms.group-related-inputs-in-a-fieldset (U.S. Web Design System (USWDS)); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); modern.cards-are-a-choice-not-a-default (Stan Kirilov); canon.tufte-one-plus-one-equals-three (Edward Tufte); craft.separation-order-space-then-lines-then-boxes (Adam Wathan & Steve Schoger; Erik D. Kennedy). + +## 21. `craft.dont-overlook-empty-states-teach-by-example` + +Design the first-load and empty states with sample data or an example, a message and a call to action; never a blank page. + +- **Source:** Erik D. Kennedy, *4 Rules for Intuitive UX* — https://www.learnui.design/blog/4-rules-intuitive-ux.html +- **In their words:** "Use the 'first load' experience to provide sample data, showing by example what the properly-working app will look like" +- **Do:** Show a designed empty state with message, example and call to action; Seed sample data on first load; Show examples rather than descriptions +- **Don't:** Show a totally blank page on first load; Show a table header with no rows and no message +- **Look at:** In the journey's declared empty state, find list or table containers with 0 data children; require a visible sentence inside or adjacent and at least one actionable element. +- **Unless:** Zero-hit search results need a message but no sample data; Transient loading states +- **Also stated as:** modern.every-state-is-designed (Vercel Labs); writing.empty-states-say-what-next (GitHub Primer); data-display.empty-data-explains-and-offers-next-step (IBM Carbon Design System); data-display.wrap-before-truncating-and-reveal-the-rest (GitHub Primer). ## 22. `craft.spacing-and-sizing-system` @@ -251,17 +255,19 @@ Draw every margin, padding and gap from one scale, and make space between groups - **Don't:** Use one-off values such as 13px or 27px; Use equal gaps across a group boundary - **Look at:** Collect all computed margin, padding and gap values above 0; report distinct values and how many are off a 4px grid; compare label-to-field gap with field-to-next-label gap. - **Unless:** Optical adjustments on icons and hanging punctuation sit a pixel or two off-scale -- **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen); modern.spacing-comes-from-a-scale (Stan Kirilov). +- **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.spacing-comes-from-a-scale (Stan Kirilov). -## 23. `craft.separation-order-space-then-lines-then-boxes` +## 23. `craft.align-with-readability-in-mind` -Use the lightest separator that works: more space first, then a keyline or background band, and a box only for the object that is acted on. +Keep few strong left edges, right-align comparable numbers with tabular figures, and hang punctuation so edges line up. -- **Source:** Adam Wathan & Steve Schoger; Erik D. Kennedy, *Refactoring UI (fewer-borders tactic); 7 Rules for Creating Gorgeous UI, Part 1* — https://www.refactoringui.com/ (folklore: the wording is not verified) -- **Do:** Separate with whitespace by default; Add a keyline or background band only when space alone fails; Box only the object a person acts on -- **Don't:** Reach for a card or border first -- **Look at:** Between sibling groups, record which separator is used: gap at least 2x the inner gap (space), hr or border-bottom (line), bordered or shadowed wrapper (box); report the inner/outer gap ratio. -- **Unless:** Dense data such as tables where zebra stripes or keylines are used; Interactive cards that are the unit of action +- **Source:** Steve Schoger, *Little UI Details (tweet, 15 Jun 2017)* — https://digitalsynopsis.com/design/useful-ui-ux-design-tips/ +- **In their words:** "Aligning text is an easy way to clean up your design and make your content much more scannable." +- **Do:** Keep few, strong left edges; Right-align numeric columns with tabular figures; Hang bullets, icons and punctuation so text edges align +- **Don't:** Align a right-aligned image to ragged left-aligned text; Centre columns of numbers +- **Look at:** Count distinct left-edge x positions of text blocks in a container at 1px tolerance; for td cells matching a number or currency pattern, text-align must be right or end and tabular figures must be set. +- **Unless:** IDs, ZIP codes and phone numbers stay left-aligned; Centred layouts under 3 lines +- **Also stated as:** data-display.right-align-numbers-tabular-figures (GitHub Primer); data-display.text-left-headers-follow-their-column (W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.)). ## 24. `craft.wcag-contrast-and-dont-rely-on-colour-alone` @@ -273,21 +279,9 @@ Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dar - **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only - **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. - **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself -- **Also stated as:** modern.contrast-and-not-colour-alone (Apple). - -## 25. `craft.dont-overlook-empty-states-teach-by-example` - -Design the first-load and empty states with sample data or an example, a message and a call to action; never a blank page. - -- **Source:** Erik D. Kennedy, *4 Rules for Intuitive UX* — https://www.learnui.design/blog/4-rules-intuitive-ux.html -- **In their words:** "Use the 'first load' experience to provide sample data, showing by example what the properly-working app will look like" -- **Do:** Show a designed empty state with message, example and call to action; Seed sample data on first load; Show examples rather than descriptions -- **Don't:** Show a totally blank page on first load; Show a table header with no rows and no message -- **Look at:** In the journey's declared empty state, find list or table containers with 0 data children; require a visible sentence inside or adjacent and at least one actionable element. -- **Unless:** Zero-hit search results need a message but no sample data; Transient loading states -- **Also stated as:** modern.every-state-is-designed (Vercel Labs). +- **Also stated as:** modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group). -## 26. `craft.hierarchy-is-everything-squint-test` +## 25. `craft.hierarchy-is-everything-squint-test` Squint: the most important thing must catch the eye first and the least important last; one element dominates each screen. @@ -298,7 +292,7 @@ Squint: the most important thing must catch the eye first and the least importan - **Look at:** Given a declared primary action, score every interactive element as area x contrast x font-weight factor; the declared primary must rank first, and no set of buttons may sit within 10% of each other. - **Unless:** Page titles are the only element styled all-out up-pop; A browsing page such as a gallery may have no single most important thing -## 27. `craft.greys-dont-have-to-be-grey-never-use-black` +## 26. `craft.greys-dont-have-to-be-grey-never-use-black` Tint the neutral scale toward the brand hue and never use pure black for text. @@ -309,7 +303,7 @@ Tint the neutral scale toward the brand hue and never use pure black for text. - **Look at:** Count text, border and background colours with saturation 0 and lightness under 15%, and the share of neutral swatches with saturation exactly 0. - **Unless:** It does not always work; worth a trial; High-contrast or accessibility modes and print -## 28. `craft.type-scale-few-font-sizes` +## 27. `craft.type-scale-few-font-sizes` Use about four font sizes from a fixed scale; reuse the default size for body, menus, lists and controls. @@ -320,18 +314,7 @@ Use about four font sizes from a fixed scale; reuse the default size for body, m - **Look at:** Count distinct computed font-size values on visible text per viewport and how many fall outside the declared scale. - **Unless:** Sizes must stay distinguishable, so large text may step many points apart; Marketing pages may add a display size -## 29. `craft.align-with-readability-in-mind` - -Keep few strong left edges, right-align comparable numbers with tabular figures, and hang punctuation so edges line up. - -- **Source:** Steve Schoger, *Little UI Details (tweet, 15 Jun 2017)* — https://digitalsynopsis.com/design/useful-ui-ux-design-tips/ -- **In their words:** "Aligning text is an easy way to clean up your design and make your content much more scannable." -- **Do:** Keep few, strong left edges; Right-align numeric columns with tabular figures; Hang bullets, icons and punctuation so text edges align -- **Don't:** Align a right-aligned image to ragged left-aligned text; Centre columns of numbers -- **Look at:** Count distinct left-edge x positions of text blocks in a container at 1px tolerance; for td cells matching a number or currency pattern, text-align must be right or end and tabular figures must be set. -- **Unless:** IDs, ZIP codes and phone numbers stay left-aligned; Centred layouts under 3 lines - -## 30. `canon.rams-understandable` +## 28. `canon.rams-understandable` The structure of the product should be visible and its controls legible without instruction. @@ -341,7 +324,29 @@ The structure of the product should be visible and its controls legible without - **Don't:** Rely on tooltips as the only explanation; Ship icon-only controls without a visible label; Require a manual - **Look at:** Interactive elements with no accessible name; icon-only controls without a visible text label; landmark regions with no heading. - **Unless:** Rams's own products rely on convention rather than labels everywhere; the rule is about structure, not text density -- **Also stated as:** modern.no-junk-drawer-menus-or-unlabeled-icons (Jakob Nielsen (NN/G)); usability.dont-make-me-think (Steve Krug); craft.labels-are-a-last-resort (Steve Schoger (attendee notes by ynotdraw)). +- **Also stated as:** modern.no-junk-drawer-menus-or-unlabeled-icons (Jakob Nielsen (NN/G)); writing.buttons-name-the-action (IBM Carbon Design System); usability.dont-make-me-think (Steve Krug); craft.labels-are-a-last-resort (Steve Schoger (attendee notes by ynotdraw)). + +## 29. `canon.rams-as-little-design-as-possible` + +Remove everything that does not serve the purpose; less, but better. + +- **Source:** Dieter Rams, *Ten principles for good design* — https://www.vitsoe.com/us/about/good-design +- **In their words:** "Good design is as little design as possible. Less, but better – because it concentrates on the essential aspects, and the products are not burdened with non-essentials." +- **Do:** Remove anything that does not serve the purpose; Keep one border where one will do +- **Don't:** Add ornament or decoration for its own sake; Repeat the heading as a decorative icon; Wrap content in extra bordered or shadowed layers +- **Look at:** Decorative-only elements (no text, image or interactive role, only border/shadow/background) and distinct box-shadow, border-radius and gradient declarations in computed styles. +- **Unless:** Aesthetic quality is part of usefulness (principle 3): stripping to nothing is not the goal + +## 30. `canon.tufte-smallest-effective-difference` + +Make every visual distinction as subtle as it can be while still reading clearly. + +- **Source:** Edward Tufte, *Visual Explanations (1997), p.73* — https://boxesandarrows.com/three-lessons-from-tufte-special-deliverable-6/ (secondary) +- **In their words:** "Make all visual distinctions as subtle as possible, but still clear and effective." +- **Do:** Light grey before dark grey, grey before colour; Dividers and borders lighter than body text; Mute secondary elements +- **Don't:** Black borders and saturated dividers around quiet content; Secondary elements with more contrast than the primary content +- **Look at:** Luminance contrast of non-data, non-text elements (borders, dividers, gridlines, rules) against their background; structural lines above a lens threshold such as 3:1; secondary elements exceeding the primary content's contrast. +- **Unless:** The distinction must still be clear and effective; WCAG text contrast is a floor this rule must not undercut ## 31. `canon.bringhurst-leading-is-a-rhythmic-unit` @@ -376,18 +381,7 @@ A library or AI default is someone else's decision; commit to one written aesthe - **Unless:** A purple brand is allowed to be purple; the tell is the constellation and the absence of a decision, not any one colour - **Also stated as:** craft.supercharge-the-defaults (Steve Schoger (attendee notes by ynotdraw)). -## 34. `modern.cards-are-a-choice-not-a-default` - -Cards are for browsing; use lists for searching and tables for comparing, and keep each card readable with one primary action. - -- **Source:** Stan Kirilov, *UI Card Design (StanVision journal)* — https://www.stan.vision/journal/ui-card-design-examples-best-practices-and-common-patterns (secondary) -- **In their words:** "Cards for browsing. Lists for searching. Tables for comparing." -- **Do:** Lists for ranked or homogeneous data, tables for comparison; Consistent card heights; The title as the link, with a pseudo-element extending the hit area -- **Don't:** Card grids for search results and settings; Whole-card anchor wrappers; More than 3 clickable elements per card; Nested cards -- **Look at:** Detect card grids (3+ siblings with equal border, shadow and radius); fail on text under 14px, over 3 interactive descendants per card, row heights differing over 20%, or an anchor wrapping more than heading plus paragraph. -- **Unless:** Masonry where variable height is the design choice; Media browsing such as Netflix or Pinterest - -## 35. `modern.frequent-actions-do-not-animate` +## 34. `modern.frequent-actions-do-not-animate` Actions used many times a day, and anything keyboard-triggered, appear instantly without an enter animation. @@ -399,6 +393,17 @@ Actions used many times a day, and anything keyboard-triggered, appear instantly - **Unless:** macOS context menus fade out and blink the chosen item; Rare features may be theatrical (delight-impact curve) - **Also stated as:** modern.delight-scales-with-rarity (Benji Taylor). +## 35. `modern.numbers-and-text-do-not-shift-layout` + +Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. + +- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ +- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" +- **Do:** tabular-nums in tables, timers and prices; width and height on every img +- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover +- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. +- **Unless:** Prose numbers + ## 36. `modern.motion-values-proportional-to-trigger` Scale and fade motion starts near its resting size, in proportion to the trigger, never from zero or a heavy squash. @@ -475,18 +480,7 @@ Every focusable element shows a visible focus ring on :focus-visible, and no sti - **Don't:** outline: none or 0 with no replacement; Focus rings hidden under sticky headers - **Look at:** Tab through every focusable element; diff the focused and unfocused rect or read computed outline and box-shadow under :focus-visible; check the focused rect is not intersected by position fixed or sticky elements above it. -## 43. `modern.numbers-and-text-do-not-shift-layout` - -Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. - -- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ -- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" -- **Do:** tabular-nums in tables, timers and prices; width and height on every img -- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover -- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. -- **Unless:** Prose numbers - -## 44. `modern.shadows-share-one-light-source` +## 43. `modern.shadows-share-one-light-source` All shadows on a page share one light direction and offset ratio, layered and tinted toward the background hue. @@ -497,7 +491,7 @@ All shadows on a page share one light direction and offset ratio, layered and ti - **Look at:** Collect every computed box-shadow; fail when offset signs mix across the page or distinct shadow strings exceed about 6; warn when shadow hue is far from the background hue with alpha above 0.5. - **Unless:** Inset shadows for sunken fields; Glows meant as glows -## 45. `modern.glass-and-blur-earn-their-place` +## 44. `modern.glass-and-blur-earn-their-place` Translucency is for chrome over busy backgrounds with heavy blur and a contrast fallback, never for body content or decoration. @@ -507,3 +501,213 @@ Translucency is for chrome over busy backgrounds with heavy blur and a contrast - **Don't:** Glass cards for body content; Low blur over photos; Text whose contrast depends on what scrolls beneath it; Glass with a neon glow as decoration - **Look at:** For each element with backdrop-filter, sample contained-text contrast against the composited background at three scroll positions; any under 4.5:1 fails; blur under 12px over an image warns; body text over 2 lines inside glass warns. - **Unless:** OS-level materials that honour the user's Reduce Transparency setting + +## 45. `color.from-tokens-not-hex` + +Every colour on the page comes from the design system's named tokens or palette functions, never from hex values copied into components. + +- **Source:** GOV.UK Design System (Government Digital Service), *Styles: Colour* — https://design-system.service.gov.uk/styles/colour/ +- **In their words:** "Do not copy the specific hexadecimal (hex) colour values." +- **Do:** Reference colour by role token (brand, text, error, border) rather than by value; Use palette colours (tints and shades of a few families) for supporting elements; Use a functional token only in the context it is designed for +- **Don't:** Hex literals in component styles; Near-duplicate colours (#1d70b8 next to #1d70b9) created by eye-dropping; Using the error token as a general red +- **Look at:** Collect every computed color, background-color, border-*-color, outline-color and fill/stroke of painted elements, and every value of CSS custom properties declared on :root (and on any theme selector). Count distinct painted colours that match no custom-property value (exact RGBA after resolution), and count pairs of painted colours closer than ΔE 2 that are not identical. +- **Unless:** Images, illustrations and embedded third-party widgets; Browser defaults on unstyled native controls; GOV.UK: palette colours (not functional ones) are allowed for illustrations and custom components +- **Also stated as:** color.mode-aware-tokens-in-every-theme (GitHub Primer). + +## 46. `color.status-colours-keep-their-meaning` + +Each status colour has one meaning across the product (critical for errors and blocked actions, warning for what needs attention, success for what went well, info for tips) and is never borrowed for promotion or decoration. + +- **Source:** Shopify Polaris, *Colors: Palettes and roles (Critical, Success)* — https://polaris.shopify.com/design/colors/palettes-and-roles +- **In their words:** "Elements using critical must convey messaging that implies that an action is impossible, blocked, or has resulted in an error." +- **Do:** Map error, warning, success and info to named roles or tokens and use them only in those roles; Reserve the critical red for errors, blocked actions and destructive buttons; Use the info role, not warning or critical, for tips and announcements +- **Don't:** A sale or 'new' badge in the error red; Success green used to entice or to advertise an offer; Warning colour for 'coming soon' or 'under construction' messaging; Two different reds meaning error on different screens +- **Look at:** Find the colour the page uses for error text (an element with role=alert, aria-invalid's described-by message, or a class/token named error/critical/danger) and the success colour likewise. Count painted elements (text, fill or border) whose colour equals that error or success colour, within a ΔE of 3, and that are neither a validation message, an invalid field, a status badge of that meaning, nor a destructive action. +- **Unless:** Brand colours that happen to be red, as long as a separate, distinct error red is used for errors; Data visualisations where a series colour coincides with a status hue but no status is implied (prefer avoiding it) + +## 47. `color.one-action-colour-apart-from-status` + +Links and primary actions share one interactive colour family, used consistently, and that colour is not the colour of errors, warnings or success. + +- **Source:** IBM Carbon Design System, *Elements: Color, Overview (Color anatomy)* — https://carbondesignsystem.com/elements/color/overview/ +- **In their words:** "The core blue family serves as the primary action color across all IBM products and experiences. Additional colors are used sparingly and purposefully." +- **Do:** One link colour, used only for links; Primary buttons in the brand or action colour; Danger colour reserved for destructive buttons, not for the primary action +- **Don't:** Links in the error red; Primary buttons in several different hues across screens; Static text coloured like links +- **Look at:** Take the computed text colour of every a[href] in running text and the background of every primary (first, filled, or type=submit) button. Count distinct hues among them (30° bins), and count links or primary buttons whose colour is within ΔE 10 of the page's error, warning or success colour. Also count non-interactive text elements painted in the link colour. +- **Unless:** Primer (GitHub) deliberately uses the success role for primary buttons; a system that states such a mapping consistently is following its own rule; Destructive primary actions (Delete account) take the danger colour on purpose; Navigation menus, where position signals the link + +## 48. `color.controls-and-graphics-3-to-1` + +The parts that show a control is there and what state it is in (input borders, checkbox boxes and ticks, toggle tracks, icon-only buttons, meaningful chart marks) contrast at least 3:1 with the colours next to them. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "Unless the control is inactive, any visual information provided that is necessary for a user to identify that a control is present and how to operate it must have a minimum 3:1 contrast ratio with the adjacent colors." +- **Do:** Input borders at 3:1 against the background the input sits on, or a filled input background at 3:1; Checkbox ticks and radio dots at 3:1 against the box; Standalone icons at 3:1 against their background; Avoid very thin lines that anti-alias below the nominal ratio +- **Don't:** Pale grey input borders (#ddd on white is about 1.4:1); A selected state shown only by a faint tint; Hover effects that lower a control's contrast with its surroundings +- **Look at:** For each visible input, select, textarea, checkbox, radio, [role=switch] and icon-only button (no visible text): compute the contrast ratio between the colour that identifies it (border colour, or its own background when it has no border, or the icon fill) and the background behind it; count those below 3:1, unrounded. Disabled controls are skipped. +- **Unless:** Inactive (disabled) controls are exempt; A control identified by its visible text needs no contrasting boundary; Logos and decorative graphics; Appearance determined by the browser and not modified by the author + +## 49. `color.focus-ring-contrasts-with-its-surroundings` + +The focus indicator contrasts at least 3:1 with whatever it is drawn against: the page background for an outer ring, the component's own colours for an inner one. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast, Relationship with Focus Visible (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." +- **Do:** An outer ring that contrasts with the page background; A two-colour ring (dark and light) that holds on any background; A thick indicator rather than a 1px one +- **Don't:** A yellow outer ring on a white page; A focus border that changes hue inside the component without contrasting with its fill; Focus shown only by a background tint change +- **Look at:** Tab to each focusable control; diff the focused and unfocused screenshots of the control's box padded by a few pixels; for the changed pixels, take their colour and the colour of the unchanged pixels adjacent to them (page background outside, component fill inside). Count controls where no changed region reaches 3:1 against its adjacent colour. page.focus-visible checks only that some pixel changes; this checks that the change can be seen. +- **Unless:** Unmodified browser default focus styles; WCAG does not compare focused and unfocused states with each other; a background-only change is out of scope for 1.4.11 but fails Use of Color + +## 50. `color.few-families-in-proportion` + +Use a few colour families in a deliberate proportion (neutral base dominant, then primary, secondary and a small accent), not an even spread of many hues. + +- **Source:** U.S. Web Design System (GSA), *Design tokens: Theme color tokens* — https://designsystem.digital.gov/design-tokens/color/theme-tokens/ +- **In their words:** "about 60% of your site’s color would be the primary color family, about 30% would be the secondary color family, and about 10% would be the accent color families" +- **Do:** Neutral base for text and most surfaces; One primary family carrying most of the colour, one secondary, a small accent; Additional colours used sparingly and for a purpose (Carbon); Start in black and white, then add colour to support the message (USWDS) +- **Don't:** Five or more saturated hue families at similar weight on one screen; An accent that covers more area than the primary; A new hue introduced for a single component +- **Look at:** Screenshot the page and bucket every non-neutral pixel (HSL saturation above about 15%) by hue into 30° bins, ignoring images and status colours. How many hue families take more than 2% of the coloured area, and does the largest one carry most of it while the smallest (accent) stays near a tenth? +- **Unless:** USWDS: the proportions are for non-base colours; neutral text will usually dominate; Illustration, photography and data visualisation, which need their own palettes; Brands whose identity is multi-hue + +## 51. `writing.one-label-per-action` + +One concept, one word: controls that do the same thing carry the same label everywhere, and controls that do different things never share a label. + +- **Source:** GitHub Primer, *Accessibility guide: Descriptive buttons, 'How to test names'* — https://primer.style/guides/accessibility/descriptive-buttons +- **In their words:** "When buttons perform the same action, they have the same name." +- **Do:** Keep a terminology list of preferred words and words not to use for the product; Pick one verb per action (Delete or Remove, not both for the same thing) and reuse it across pages; Add the object to disambiguate repeated actions ('Remove Apples', 'Remove Pears') +- **Don't:** Synonyms for one action across screens: 'Save' here, 'Update' there, 'Apply' elsewhere; Identical labels for different actions on the same page; Naming the same object two ways ('workspace' and 'project') in one product +- **Look at:** Across the journey's pages, collect (accessible name, action) pairs for buttons and links, where action is the form action/href/handler target. Count names that map to two or more different actions on one page, and actions reached by two or more different names across pages; also flag known synonym pairs present together (save/update/apply, delete/remove, sign in/log in, cart/basket). Count must be 0. +- **Unless:** Delete and Remove may coexist when they mean different things (destroy vs take out of a collection), as Carbon defines them — then each must be used only for its own meaning + +## 52. `writing.dates-numbers-units-for-the-reader` + +Dates spell out the month, numbers are numerals with thousands separators, and units sit a space after their number — formatted in the reader's locale, never as an ambiguous all-numeric date or a raw machine value. + +- **Source:** Shopify Polaris, *Content: Grammar and mechanics, 'Numbers, dates, and currency'* — https://polaris.shopify.com/content/grammar-and-mechanics +- **In their words:** "Use the month’s full name. If there isn’t enough space, use 3-letter abbreviations. Don’t write dates with numerals only." +- **Do:** 'December 11, 2024' or 'Dec 11, 2024' (in the reader's locale order); Numerals, not words: 'You have 5 orders to fulfill'; Thousands separators: '12,000'; A space between number and unit: '3.4 lb', '2 kg'; Currency code after the amount when currencies can be confused: '$10,000 USD'; Format with Intl.DateTimeFormat / Intl.NumberFormat for the user's locale +- **Don't:** All-numeric dates like '12/11/24'; ISO timestamps or epoch values shown raw ('2024-12-11T09:30:00Z'); Ordinals in dates ('January 23rd'); Unit glued to the number ('3.4lb'); Shortened numbers like '12 k' where the exact value matters +- **Look at:** Scan visible text nodes. Count matches of all-numeric dates (\b\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}\b), raw ISO timestamps (\d{4}-\d{2}-\d{2}T\d{2}:), integers of 5+ digits with no separator outside codes/IDs, and numbers glued to a unit (\d(kg|lb|cm|mm|km|mi|ml|oz)\b). Count must be 0. +- **Unless:** Polaris notes these are American English base rules and dates, numbers and measurements should be localized automatically — the target is the reader's locale, not US format; Identifiers, codes, SKUs and years are not quantities and take no separator; Dense data tables may use compact numeric dates if the format is unambiguous for the locale and stated in the column header + +## 53. `data-display.no-tables-for-layout` + +A table is for comparing data in rows and columns, never for arranging content on the page; layout belongs to the grid. + +- **Source:** GOV.UK Design System (Government Digital Service), *Table — When not to use this component* — https://design-system.service.gov.uk/components/table/ +- **In their words:** "Never use the table component to layout content on a page." +- **Do:** CSS grid or flex for page and dashboard layout; A list, cards or a summary list for items that do not share columns; Tables only where every row has the same fields +- **Don't:** A table that positions a sidebar, form or dashboard tiles; Table cells holding headings, paragraphs or whole forms; role=presentation on a table that actually holds tabular data +- **Look at:** Count table elements that have no th and either contain headings, more than one paragraph per cell, form fieldsets or nested tables, or have a single row whose cells hold unrelated content blocks. +- **Unless:** HTML email, where tables are still the only reliable layout tool + +## 54. `data-display.one-unit-and-format-per-column` + +Every value in a column uses the same unit, precision and format; the unit is named once in the header, not repeated in each cell. + +- **Source:** U.S. Web Design System (GSA), *Table — Usability guidance* — https://designsystem.digital.gov/components/table/ +- **In their words:** "Predictably format columns. Take care not to vary units or formatting within the same column. Instead, normalize values so they can be easily compared." +- **Do:** Normalise a column to one unit (all days, not days and weeks); Keep the same number of decimal places in every row of a column; Put the unit or symbol in the column header ('Temperature °C') rather than in every cell; Use one date format and one thousands separator throughout the table +- **Don't:** Mixing '3 days' and '2 weeks' in one column; '12.5', '12.50' and '12.500' in the same column; Abbreviating some values ('1.2k') and not others in the same column +- **Look at:** For each column of numeric td, count distinct decimal-place counts, distinct unit suffixes or prefixes, and distinct date formats; any column with more than one is a failure. +- **Unless:** A column deliberately mixing magnitudes where the unit changes are labelled per cell and sorting is by underlying value — rare, and worth questioning + +## 55. `data-display.header-cells-are-th-with-scope` + +A data table marks its header cells as th and its data cells as td; when headers run both across and down, scope (or id/headers in complex tables) ties each cell to its headers. + +- **Source:** W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.), *Tables Tutorial* — https://www.w3.org/WAI/tutorials/tables/ +- **In their words:** "Header cells must be marked up with <th>, and data cells with <td> to make tables accessible. For more complex tables, explicit associations may be needed using scope, id, and headers attributes." +- **Do:** A thead row of th scope=col in every data table; th scope=row for the first cell when it names the row's item; id and headers attributes when a cell sits under more than one level of header; A caption that names the table +- **Don't:** Header rows built from td styled bold; Div grids that look like tables with no role=table/row/columnheader/cell semantics; One table holding several topics separated by extra rows of th +- **Look at:** Count table (and role=table/grid) elements that have more than one row of data and no th / role=columnheader; count tables with both a header row and a header column whose th lack scope; count tables with no caption, aria-label or aria-labelledby. +- **Unless:** A simple table with one header row needs only th, not scope; scope matters once there are header rows and header columns; Layout tables, which should not exist (see data-display.no-tables-for-layout) + +## 56. `data-display.long-tables-sort-and-say-so` + +Long tables let users sort the columns that have a natural order, start with one column sorted, and expose the sort state to assistive technology. + +- **Source:** U.S. Web Design System (GSA), *Table — Usability guidance* — https://designsystem.digital.gov/components/table/ +- **In their words:** "Enable sort where useful. Add row sorting to individual columns of long tables where the data can be logically ordered either alphabetically or numerically." +- **Do:** Sort controls as buttons inside the th of sortable columns; One column sorted on load, shown with an arrow and aria-sort on its th (Primer); A polite live region that announces the new sort (USWDS); A filter or search field above long tables, and pagination for very large sets +- **Don't:** Long tables with no sort, filter or pagination; A sort state shown only by an icon with no aria-sort; Row sorting on tables with merged (colspan/rowspan) cells +- **Look at:** Count tables with more than 20 body rows (or with pagination) that have no sort control in any th and no filter/search input associated with them; among sortable tables, count those where no th carries aria-sort other than none. +- **Unless:** Short tables a user takes in at a glance; Tables whose row order is itself the meaning (a ranked list, a timetable) may fix the order + +## 57. `data-display.summary-list-for-key-value-facts` + +A set of facts about one thing — label and value pairs — is shown as a summary list (dl with dt and dd), not as a table with no headers or as loose text. + +- **Source:** GOV.UK Design System (Government Digital Service), *Summary list — When to use / When not to use this component* — https://design-system.service.gov.uk/components/summary-list/ +- **In their words:** "Use a summary list to show information as a list of key facts. … only use it to present information that has a key and at least one value." +- **Do:** dl with dt (the key) and dd (the value) for record details, metadata and check-your-answers pages; A row action ('Change') whose accessible name includes the key ('Change name'); Headings or cards to separate several summary lists on one page +- **Don't:** A two-column table without th used to show one record's fields; Key–value pairs set as 'Label: value' runs in a paragraph; A summary list for genuinely tabular data or a plain list of items +- **Look at:** Where the screen shows the fields of a single record (a profile, an order, an item's metadata, a check-answers page), are the label–value pairs marked up as dl/dt/dd with each key visually distinct from its value — and is tabular data comparing several records in a table rather than a summary list? +- **Unless:** Two or three facts inside a card may be inline text if they are not scanned as a set + +## 58. `forms.error-summary-at-the-top` + +After a failed submit, show an error summary at the top of the page that takes focus and links each error to its field — even when there is only one error. + +- **Source:** GOV.UK Design System, *Error summary component* — https://design-system.service.gov.uk/components/error-summary/ +- **In their words:** "Always show an error summary when there is a validation error, even if there’s only one." +- **Do:** A summary above the h1 (below any back link) with a heading such as 'There is a problem'; Move keyboard focus to the summary when it appears; One link per error, pointing at the field (or the first field of a date or radio group); Word each summary item exactly like the message beside its field; Prefix the page <title> with 'Error: ' +- **Don't:** Errors shown only beside fields far down a long form, with focus left on the submit button; A summary of plain text with no links to the fields; A toast or banner that disappears before it can be read +- **Look at:** Submit the form empty. Within 1 s: is there an element above the first h1 of main that has focus (document.activeElement inside it), contains one link per invalid field, and does each link's href resolve to the id of an invalid input? Does document.title start with 'Error'? +- **Unless:** Primer suggests the interactive summary only for 3 or more errors, and otherwise focusing the first invalid field; A one-field form (search, single email sign-up) can rely on the message beside the field + +## 59. `forms.validate-when-the-user-is-done` + +Do not show an error while the user is still typing; validate when they try to continue, and only add earlier validation where research shows it helps. + +- **Source:** GOV.UK Design System, *Recover from validation errors pattern — When to tell the user about validation errors* — https://design-system.service.gov.uk/patterns/validation/ +- **In their words:** "Generally speaking, avoid validating the information in a field before the user has finished entering it. This sort of validation can cause problems - especially for users who type more slowly." +- **Do:** Validate on Continue or Submit; After a failed submit, update a field's error live once the user fixes it (Primer); A character count is the accepted exception: warn as the limit is passed +- **Don't:** An error that appears on the first keystroke of an email or phone field; Red borders on an untouched form at load; Browser-native HTML5 validation bubbles in place of designed messages +- **Look at:** Focus an email or formatted field and type one character, keeping focus. Wait 1 s. Does any error message, aria-invalid=true or error colour appear before blur or submit? Also load the form fresh: are any fields already marked invalid? +- **Unless:** GOV.UK goes further and says not to validate on blur either; Baymard (usability.inline-validation-after-leaving-field) recommends blur validation for hard fields — the schools agree only on 'not while typing'; Primer allows validating as the user types once the field has already been flagged invalid, so the error clears as soon as it is fixed + +## 60. `forms.keep-answers-after-an-error` + +When a submit fails, show the form again with every answer the user gave still in it — the failing ones and the passing ones. + +- **Source:** GOV.UK Design System, *Error message component — How it works* — https://design-system.service.gov.uk/components/error-message/ +- **In their words:** "Do not clear any form fields when showing the Error message component. Keep both passing and failing answers." +- **Do:** Re-render server-side errors with submitted values filled in; Keep the failing value so the user can see and edit what went wrong; Pre-populate fields when the user goes back to change an answer +- **Don't:** A reload that empties the form after a server error; Clearing the field that failed; Clearing password-adjacent fields such as name and email along with the password +- **Look at:** Fill every field with valid values except one, submit, and after the error page paints read each input's value: how many fields that had a value now read empty? +- **Unless:** Password and card security code fields may be cleared for security, and the message should say so + +## 61. `forms.mark-optional-fields-in-words` + +Ask mostly required questions and label the exceptions '(optional)' in words; do not mark required fields with asterisks. + +- **Source:** GOV.UK Design System, *Question pages pattern* — https://design-system.service.gov.uk/patterns/question-pages/ +- **In their words:** "in most contexts, add ‘(optional)’ to the labels of optional fields" +- **Do:** '(optional)' in the label, or in the legend for a radio or checkbox group; Removing optional questions before marking them; One convention across the whole service +- **Don't:** Asterisks on mandatory fields; Optional fields that look identical to required ones; Required and optional marked differently on different pages +- **Look at:** Submit the form with every field empty and note which fields raise no error (optional). Count optional fields whose label or legend text lacks 'optional', and labels that contain '*'. +- **Unless:** USWDS instead marks required fields with a red asterisk explained by a note at the top, and also labels optional fields '(optional)' — the schools agree that optional fields say so in words; Primer and USWDS exempt one-field forms and login forms, where every field is plainly required + +## 62. `forms.input-type-matches-the-answer` + +Give each field the type and on-screen keyboard its answer needs — email, tel, inputmode numeric or decimal for numbers — and avoid type=number. + +- **Source:** GOV.UK Design System, *Text input component — Numbers* — https://design-system.service.gov.uk/components/text-input/ +- **In their words:** "With <input type="number"> there’s a risk of users accidentally incrementing a number when they’re trying to do something else - for example, scroll up or down the page." +- **Do:** type='email' with spellcheck=false for email addresses; type='tel' for phone numbers; type='text' with inputmode='numeric' for whole numbers and codes, inputmode='decimal' for amounts; spellcheck=false on names, references and codes +- **Don't:** type='number' for card numbers, postcodes, account numbers or reference codes; type='text' with no inputmode on a field that only accepts digits; Blocking paste into any field +- **Look at:** For each input, read type and inputmode against its label: count email fields not type=email, phone fields not type=tel, digit-only fields (amount, quantity, code, card, account) with neither inputmode numeric/decimal nor type=tel, and any type=number. +- **Unless:** type=number when research shows a need, such as a stepper on a small bounded quantity; Negative amounts: GOV.UK advises type=text without inputmode, since some keypads have no minus key + +## 63. `forms.ask-only-what-you-need` + +Every question on the form has a named use; drop the ones nobody can justify, and never ask for the same thing twice in one journey. + +- **Source:** GOV.UK Design System, *Question pages pattern* — https://design-system.service.gov.uk/patterns/question-pages/ +- **In their words:** "You should make sure you know why you’re asking every question and only ask users for information you really need." +- **Do:** A question protocol: for each field, who uses the answer and what for; Reuse an earlier answer by pre-filling it rather than asking again; Let 'I do not know' be an answer where it is a valid one; Say why a sensitive question is asked, in its hint +- **Don't:** 'Nice to have' fields such as title, gender or 'how did you hear about us' with no stated use; Asking for an email or address a second time in the same journey; Confirm-email fields that make the user type the address twice, instead of playing it back for checking +- **Look at:** List every field on the form. For each, can the team name who reads the answer and what decision it changes? How many fields have no answer, and how many repeat information given earlier in the journey? +- **Unless:** Legal or regulatory questions whose use is mandated, which should still say why in a hint diff --git a/.claude/skills/uxcli/references/research.md b/.claude/skills/uxcli/references/research.md new file mode 100644 index 0000000..e98ccd5 --- /dev/null +++ b/.claude/skills/uxcli/references/research.md @@ -0,0 +1,61 @@ +# research — learning the domain before you design for it + +Read when: you are about to design for a product whose field you do not already know from sources — +its vocabulary, its rules, what its people do all day and what goes wrong for them. A screen designed +from what a model assumes about a field looks plausible and fails the person who works in it. This is +how you find out first, and how you write down what you found so the next agent does not have to. + +## Start from the kind of product + +```bash +npx -y @junixlabs/uxcli template # workspace, shop, landing +npx -y @junixlabs/uxcli template show <id> # screens and their lens, journeys to walk, what to research +npx -y @junixlabs/uxcli template apply <id> # the actor questions, written as unknowns (creates only) +``` + +A template is where research starts, not what it found: every line about people in it is a question. +If no template fits, take the closest one's questions as a checklist and say which you skipped. + +## Where to look, in order of how much it can carry + +| Rank | Source | What it can tell you | `evidence` when it becomes a viewpoint or an insight | +|---|---|---|---| +| 1 | The project's own data: analytics, support tickets, interview notes, sales calls, the running product | What these people actually do and where they get stuck | insight `source.type` = analytics, interview, ticket, run | +| 2 | Regulations and standards for the field: laws, WCAG, industry rules | What must be true whatever anyone prefers | `author`, with the clause quoted | +| 3 | Published research with a method: Baymard, NN/g studies, peer-reviewed HCI | What holds across many products of this kind, and how strongly | `study` | +| 4 | Public design systems and style guides: GOV.UK, Carbon, Primer, Polaris, USWDS | Patterns that were tested and are maintained, with usage rules | `author` | +| 5 | Products people in the field already use, and their reviews | The conventions they expect and the complaints they repeat | `secondary`, the review quoted | +| 6 | Community practice: articles, talks, threads | Ideas worth testing | `folklore` — never more than a hypothesis | + +The lenses (`lenses/<kind>.md`) and the topic pools behind them are rank 3 and 4, already fetched and +quoted. Read them for the kind of screen; research is for what they cannot know — this field. + +## What to come back with + +1. **The field's words.** The terms people in the field use for its records, states and outcomes, each + with where you saw it. Labels, headings and messages use these words, not the database's. +2. **The rules.** What a regulation or standard requires of this kind of screen (consent, retention, + disclosure, accessibility), quoted with its clause. +3. **The work.** Which task is repeated most, how often, under what pressure, on which device — from + rank 1 sources if the project has them; otherwise it stays an unknown. +4. **The conventions.** How two or three products the people already use handle the same task, and what + their reviews complain about. A convention people already know is a reason, not a rule. +5. **The open questions.** Everything you could not settle, as questions in the actor's `unknowns[]`. + +## How to write it down + +- A finding about people or about this field is an insight (`references/understand.md`): one claim, + its `source`, the exact numbers or words in `evidence[]`, and what would prove it wrong. +- A finding about how screens of this kind should work is a candidate viewpoint: who said it, where, + their words. Bring it to the person running you; uxcli's pools only carry what someone can cite. +- Fetch what you cite in this session and quote it exactly. A page you could not open is not a source: + say so and leave the question open. +- Then design: `uxcli context show <journey>`, the lens for each screen the template names, and the + mockups (`references/mockups.md`). + +## What you do not do + +- Fill an unknown from what you expect the field to be like. An unanswered question stays a question. +- Treat a competitor's screen as evidence that its pattern works. It is evidence that it exists. +- Raise community advice above a hypothesis because several people repeat it. +- Quote from memory. If the words are not in something you fetched, they are not a quote. diff --git a/.claude/skills/uxcli/references/versions.md b/.claude/skills/uxcli/references/versions.md new file mode 100644 index 0000000..27e2f7a --- /dev/null +++ b/.claude/skills/uxcli/references/versions.md @@ -0,0 +1,43 @@ +# versions — proposing a better version, and showing it is better + +Read when: a walk of a journey showed something worth changing, or you are about to change a screen +that a journey goes through. A version names one walk so it is kept; a redesign proposal says what to +change and points at the evidence; the next version is compared with the last, step by step. + +## The loop + +```bash +npx -y @junixlabs/uxcli run .uxcli/journeys/<id>.json # walk it +npx -y @junixlabs/uxcli experience --page # what the person goes through, pinned on screenshots +npx -y @junixlabs/uxcli version save <id> v1 --note="as it is" # name that walk; it is kept against pruning +# write the redesign proposal (below); a person approves it; draw and build the change +npx -y @junixlabs/uxcli run .uxcli/journeys/<id>.json +npx -y @junixlabs/uxcli version save <id> v2 --note="call button above the fold" --proposal=proposals/P-0012.json +npx -y @junixlabs/uxcli experience --journey=<id> --from=v1 --to=v2 --page # both pictures of every step, what moved +``` + +## A redesign proposal + +`.uxcli/proposals/P-xxxx.json` with `"kind": "redesign"`, `target` naming the journey and step +(`journeys/<id>.json#<step>`), `statement` saying what changes for the person, and `evidence[]` citing +what the walk showed — one entry per finding it answers: + +```json +{ "run": "runs/R-…", "check": "experience.reach", "observed": "[data-uxcli=call-action] 2 scrolls away at 390x844", "what": "the call button is below the fold on a phone" } +``` + +A proposal with no evidence is an opinion; cite the walk, the lens viewpoint, or the insight it rests +on. A person approves it before it is built. Name the version that answers it with `--proposal`. + +## Saying it is better + +Report the comparison in its own words: the steps, typing, scrolls and estimate that moved, the +findings that went away and the ones that appeared. A version with fewer findings and a higher +estimate is not simply better; say both. The metrics are findings, not verdicts, and nobody was +watched: a better walk is evidence for the change, not proof that people prefer it. + +## What you do not do + +- Rewrite or delete a version. A new walk gets a new name. +- Save a version of a walk you did not make on the final files. +- Call a change better without the comparison, or show only the numbers that improved. diff --git a/.claude/skills/uxcli/templates/landing.md b/.claude/skills/uxcli/templates/landing.md new file mode 100644 index 0000000..63f82aa --- /dev/null +++ b/.claude/skills/uxcli/templates/landing.md @@ -0,0 +1,83 @@ +# Product site — the `landing` template + +Marketing sites for a product or service: a visitor decides in minutes whether to sign up, buy or get in touch. + +A template is where research starts, not what it found. Nothing below is a fact about this product's users: the screens and journeys are what products of this kind usually have, and every question is yours to answer from a source before it becomes an insight. `uxcli template apply landing` writes the actor questions into `.uxcli/understanding/actors/`; `references/research.md` in this skill is how to answer them. + +## Who uses it + +### `evaluating_visitor` + +Someone who arrived with a problem and is deciding whether this product solves it. + +- What problem were they trying to solve when they arrived, in their own words? +- Where did they come from (search, a recommendation, an ad, a comparison article)? +- What do they need to see before they believe it works for someone like them? +- What stops them from signing up today: price, effort, risk, someone else's approval? +- Who else is involved in the decision, and what will that person ask? + +### `returning_buyer` + +Someone who has seen the site before and comes back to compare plans or to buy. + +- What did they come back to check? +- Which plan details decide between two plans for them? +- What would make them contact sales instead of signing up themselves? + +## Screens, and the lens each is read against + +| Screen | Lens | What the person does there | +|---|---|---| +| Home (`home`) | `marketing` | Understands what this is, who it is for and what to do next. | +| Feature or use-case page (`feature`) | `marketing` | Sees how the product handles their specific case. | +| Pricing (`pricing`) | `marketing` | Works out what it will cost them and which plan fits. | +| Sign up (`sign-up`) | `transaction` | Creates an account with as little as is needed to start. | +| Contact sales (`contact`) | `transaction` | Asks a question and knows when they will hear back. | +| Docs or help (`docs`) | `content` | Checks a detail before committing. | + +## Journeys to walk first + +### `understand-and-sign-up` — Understand the product and sign up + +Actor `evaluating_visitor`, through: `home` → `feature` → `sign-up`. + +Watch when you walk it: + +- What the product is and who it is for is readable without scrolling at a phone viewport. +- Each view has one primary call to action, and it says what happens when pressed. +- Count the sign-up fields; each must be needed to start. + +### `compare-plans` — Choose a plan + +Actor `returning_buyer`, through: `home` → `pricing` → `sign-up`. + +Watch when you walk it: + +- Prices state the period, the currency and whether tax is included. +- The differences between plans can be read without hovering or opening anything. +- The plan chosen on pricing is the plan selected in sign-up. + +### `ask-a-question` — Ask sales a question + +Actor `returning_buyer`, through: `pricing` → `contact`. + +Watch when you walk it: + +- The contact form asks only what sales needs to reply. +- After sending, the page says what happens next and when. +- An error keeps the message the visitor typed. + +## What to research about the domain + +- Which words do people in this market use for the problem, as opposed to the product's own feature names? +- Which competitors do visitors compare against, and how do those sites explain price and value? +- What proof does this market trust: customer logos, numbers, reviews, certifications, a free trial? +- Which regulations apply to sign-up and marketing consent here? +- What questions do sales or support hear most from people who have not bought yet? + +## Where research starts + +- [Nielsen Norman Group — 10 usability heuristics](https://www.nngroup.com/articles/ten-usability-heuristics/) — The baseline vocabulary for reviewing any screen. +- [GOV.UK — content design: writing for GOV.UK](https://www.gov.uk/guidance/content-design/writing-for-gov-uk) — Researched guidance on plain language and how people read on screens. +- [Baymard Institute — research](https://baymard.com/research) — Research on account creation, form length and trust signals before purchase. +- [Deceptive Patterns (Harry Brignull)](https://www.deceptive.design/) — The catalogue of patterns that win a sign-up and lose the customer. diff --git a/.claude/skills/uxcli/templates/shop.md b/.claude/skills/uxcli/templates/shop.md new file mode 100644 index 0000000..ba464f1 --- /dev/null +++ b/.claude/skills/uxcli/templates/shop.md @@ -0,0 +1,85 @@ +# Online shop — the `shop` template + +E-commerce and marketplaces: people browse, compare, buy, and come back for their order. + +A template is where research starts, not what it found. Nothing below is a fact about this product's users: the screens and journeys are what products of this kind usually have, and every question is yours to answer from a source before it becomes an insight. `uxcli template apply shop` writes the actor questions into `.uxcli/understanding/actors/`; `references/research.md` in this skill is how to answer them. + +## Who uses it + +### `first_time_shopper` + +Someone who arrived from search or an ad and has never bought here. + +- What were they looking for when they arrived, in their own words? +- What do they need to know before trusting this shop with a payment? +- Which device do they buy on, and do they start on one device and finish on another? +- What costs or conditions would make them abandon at checkout (delivery price, delivery date, returns)? +- How do they compare products: by price, by a spec, by reviews, by photos? + +### `returning_customer` + +Someone who has bought before and comes back to buy again or to check an order. + +- Why do they come back: to reorder, to track, to return? +- Do they expect to be signed in, and what happens if they are not? +- What would they need to see to resolve an order problem without contacting support? + +## Screens, and the lens each is read against + +| Screen | Lens | What the person does there | +|---|---|---| +| Catalogue or category (`catalogue`) | `shop` | Browses a range and narrows it down. | +| Search results (`search-results`) | `shop` | Finds the product they have in mind, or a close alternative. | +| Product page (`product`) | `shop` | Decides whether this is the right product at the right total price and delivery date. | +| Cart (`cart`) | `transaction` | Checks what they are buying and what it will cost in total. | +| Checkout (`checkout`) | `transaction` | Gives delivery and payment details once and places the order. | +| Order confirmation (`confirmation`) | `transaction` | Knows the order went through and what happens next. | +| Order history (`orders`) | `workspace` | Finds an order, sees where it is, and starts a return. | + +## Journeys to walk first + +### `find-and-buy` — Find a product and buy it + +Actor `first_time_shopper`, through: `search-results` → `product` → `cart` → `checkout` → `confirmation`. + +Watch when you walk it: + +- The total cost, including delivery, is visible before the last step. +- Checkout can be finished without creating an account. +- Count the form fields in checkout; each must be needed to deliver or to charge. +- An error in checkout keeps everything the shopper already typed. + +### `compare-and-choose` — Narrow a range down to one product + +Actor `first_time_shopper`, through: `catalogue` → `product` → `catalogue` → `product`. + +Watch when you walk it: + +- Filters apply without losing scroll position, and say how many results remain. +- Going back from a product returns to the same place in the list. +- The facts people compare on are in the same place on every product page. + +### `track-and-return` — Find a past order and start a return + +Actor `returning_customer`, through: `orders` → `confirmation`. + +Watch when you walk it: + +- The order's current status and expected date are visible without opening a sub-page. +- The return starts from the order, not from a help article. +- Every step says what happens next and when. + +## What to research about the domain + +- What do shoppers in this category need to see before buying (sizes, compatibility, ingredients, warranty)? +- Which delivery, payment and return options are expected in this market, and which are legally required? +- What do customer reviews of this shop and its competitors complain about most? +- What share of traffic is on phones, and where does it come from (search, social, email)? +- Which local conventions matter: currency format, address format, phone format, tax shown or not? + +## Where research starts + +- [Baymard Institute — e-commerce UX research](https://baymard.com/research) — Large-sample usability research on product pages, search, cart and checkout. +- [Nielsen Norman Group — 10 usability heuristics](https://www.nngroup.com/articles/ten-usability-heuristics/) — The baseline vocabulary for reviewing any screen. +- [Shopify Polaris](https://polaris.shopify.com/) — Commerce patterns and content guidelines from a platform that runs many shops. +- [GOV.UK Design System — patterns](https://design-system.service.gov.uk/patterns/) — Researched patterns for addresses, payment details, errors and confirmation pages. diff --git a/.claude/skills/uxcli/templates/workspace.md b/.claude/skills/uxcli/templates/workspace.md new file mode 100644 index 0000000..f7d1513 --- /dev/null +++ b/.claude/skills/uxcli/templates/workspace.md @@ -0,0 +1,95 @@ +# B2B workspace — the `workspace` template + +CRMs, admin panels, project and ticket tools, internal back-offices: people who come back every day to work through records. + +A template is where research starts, not what it found. Nothing below is a fact about this product's users: the screens and journeys are what products of this kind usually have, and every question is yours to answer from a source before it becomes an insight. `uxcli template apply workspace` writes the actor questions into `.uxcli/understanding/actors/`; `references/research.md` in this skill is how to answer them. + +## Who uses it + +### `daily_operator` + +The person who works through records all day: a sales rep, a support agent, a coordinator. + +- Which single task do they repeat most in a day, and how many times? +- How many records do they handle per day, and how many are open at once? +- On which device and where do they do it: a desk, a phone between meetings, a site visit? +- What words do they use for the records and states, as opposed to the words the database uses? +- What do they switch to outside the product to finish a task (phone, email, chat, spreadsheet)? +- What makes them lose their place: an interruption, a page reload, a filter that resets? + +### `team_admin` + +The person who sets the product up for others: permissions, fields, imports, reports. + +- Which settings do they change after the first week, and how often? +- Who do they answer to for what the team sees in reports? +- What happens today when a permission or a field is set wrong, and who notices? + +## Screens, and the lens each is read against + +| Screen | Lens | What the person does there | +|---|---|---| +| Sign in (`sign-in`) | `transaction` | Gets into their own workspace and lands where they left off. | +| First run (empty) (`first-run`) | `workspace` | Understands what goes here and creates or imports the first record. | +| Record list (`record-list`) | `workspace` | Finds the record they need by searching, filtering or sorting, and opens it. | +| Record detail (`record-detail`) | `workspace` | Reads the record and takes the next action on it without leaving the screen. | +| Create or edit record (`record-form`) | `transaction` | Enters or corrects the record's data once, with errors caught before they save. | +| Overview dashboard (`overview`) | `data` | Sees what needs attention today and goes straight to it. | +| Settings (`settings`) | `workspace` | Changes how the workspace behaves and sees that the change took effect. | + +## Journeys to walk first + +### `first-value` — Get from sign-up to the first useful record + +Actor `daily_operator`, through: `sign-in` → `first-run` → `record-form` → `record-list`. + +Watch when you walk it: + +- The empty state says what goes here and offers the one action that starts it. +- Count the steps from landing to the first saved record; every step must earn its place. +- After saving, the new record is visible where the person expects it, without a reload. + +### `find-and-act` — Find one record and take the next action on it + +Actor `daily_operator`, through: `record-list` → `record-detail`. + +Watch when you walk it: + +- The primary action on the detail screen is reachable without scrolling at a phone viewport. +- Every action shows a change within a second; anything longer shows progress. +- Going back to the list keeps the search, filter and scroll position. + +### `triage-today` — See what needs attention today and clear it + +Actor `daily_operator`, through: `overview` → `record-detail` → `overview`. + +Watch when you walk it: + +- Numbers on the overview link to the records behind them. +- After acting on a record, the overview reflects the change when the person returns. +- Nothing on the overview needs a legend the screen does not show. + +### `change-a-setting` — Change one setting and confirm it applied + +Actor `team_admin`, through: `settings` → `record-list`. + +Watch when you walk it: + +- The setting says what it changes before it is saved. +- A destructive or team-wide change names its consequence and can be undone or is confirmed. +- The effect is visible on the screen it changes. + +## What to research about the domain + +- What is the domain's own vocabulary for records, stages and outcomes, and where is it written down? +- Which regulations or audit rules apply to the data (personal data, retention, who may see what)? +- Which competing or adjacent tools do these teams already use, and what do people praise or complain about in their reviews? +- What volume and speed does the work have: records per day, time allowed per record, peak hours? +- What does a mistake cost in this domain: a lost customer, a fine, a re-do? + +## Where research starts + +- [Nielsen Norman Group — 10 usability heuristics](https://www.nngroup.com/articles/ten-usability-heuristics/) — The baseline vocabulary for reviewing any workspace screen. +- [IBM Carbon Design System](https://carbondesignsystem.com/) — Enterprise patterns for data tables, forms, notifications and empty states, documented with usage guidance. +- [GitHub Primer](https://primer.style/) — Patterns from a tool people work in all day: navigation, lists, empty states, saving. +- [GOV.UK Design System — patterns](https://design-system.service.gov.uk/patterns/) — Researched patterns for questions, errors and confirmation, tested with real users. diff --git a/CHANGELOG.md b/CHANGELOG.md index 225f80f..adc2656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,53 @@ Entries are written per release. The log is the source material, not the text. ## [Unreleased] +### Added — the designer's habits: a library to cite, templates to start from, the walk measured + +- `uxcli experience [dir|run] [--page] [--json]`: what the person goes through on each journey's last + walk, read from the trace — steps, clicks, fields and characters typed, scrolls, the settle time of + each step in Nielsen's bands (100 ms, 1 s, 10 s), and a keystroke-level estimate of the time a + practised user needs (Card, Moran & Newell). Each workflow of a journey is totalled on its own. It + reports a wait past one second, a control the step needs that is below the fold, an answer typed + twice in one workflow, and a step that did not reach its state — every one a finding with the source + of its threshold, never a fail. With an older walk on disk it says what moved since. `--page` writes + `.uxcli/experience/index.html`: each step's screenshot with its findings pinned where the browser + found the element (a dashed bar at the bottom edge when it was below the fold). + After an error answer (4xx, 5xx or an intercepted failure) it reports an answer the page cleared — + a password excepted, the security convention — and an error nothing announced in text; within a + workflow it reports a navigation landmark whose items change order (3.2.3). The runner records, per + step, whether each typed field still held the value afterwards, which announcement roles were + visible, and each navigation landmark's items; the observer reads the input type and the landmarks. The runner now records + a typed value as its length and its hash, never the value. Gate: `experience-pairs`. +- `uxcli version [save <journey> <name>]` and `uxcli experience --journey=<id> --from=<a> --to=<b>`: + a name for one walk of a journey, kept against pruning and comparable with another, step by step, + with both pictures side by side on the page. `.uxcli/versions/<journey>/<name>.json`, one file per + version, created and never rewritten (`schemas/version.schema.json`). Proposals take + `kind: redesign`, citing the walk in `evidence[]`; `references/versions.md` is the loop. Gate: + `version-pairs`. +- `uxcli template [show <id> | apply <id> [dir]]`: templates for three kinds of product — `workspace`, + `shop`, `landing` — with the screens such products have and the lens each is read against, the + journeys to walk first and what to watch on them, the research questions and where research starts. + A template states no fact about anyone's users: every actor and research line is a question, and + `apply` writes the questions as an actor's `unknowns[]`, creating only. Gate: `template-pairs`. +- `references/research.md` in the skill: researching the product's field from sources, in the order + the evidence can carry, and writing the findings down as insights. +- Four topic pools beside the four schools, from the source repositories of GOV.UK, USWDS, Carbon, + Primer, Polaris, Mailchimp and the W3C, every quote checked verbatim: colour (10), writing and + microcopy (10), data display (12), forms (12). The lenses now carry 167 viewpoints. + +### Changed + +- VISION, PRODUCT, README and ROADMAP describe uxcli as the owner set it on 2026-10-03: an agent that + designs like a designer who did the research, in four pillars — knowledge, process, evidence and + versions, and experience validation at the centre. +- Every sentence the instrument prints is English; the example data says what the code says. + +### Fixed + +- The map's muted text read at 4.22:1 on its grey surface and failed the gate's own-pages check; it + now reads at 4.8:1 or better on every light surface. + + ### Added — one shipped skill, and three ways in - The mockups page is built from named components (`src/core/mockups-page.js`: Sidebar, Screen, diff --git a/PRODUCT.md b/PRODUCT.md index 69a1d88..facd2d0 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -5,64 +5,74 @@ brief a contributor or a reviewer holds a change against. ## Users -The person operating a coding agent on a product with an interface. Their pain is specific: the -agent designs from what it assumes about the user, and reports success with proof of failure in -hand. They want two things from an instrument: that the agent reads what the product has actually -learned about its users before it designs, and that "done" is a word the agent cannot say until a -browser has agreed. +The person operating a coding agent on a product with an interface, and the agent itself. -The agent is the second user. It calls the CLI; it reads the card; it writes the files under -`.uxcli/` that a person then signs or refuses. Everything uxcli prints is written for the agent to -act on without interpretation: `what · where · rule · check`, an exit code, a path to the packet. +Their pain is specific. The agent designs from what it assumes — about the field, about the people, +about what good looks like — and produces something plausible, generic and unwalked. Then it reports +success with proof of failure in hand. The person wants an agent that learns the field before it +draws, designs from principles somebody tested, and walks the journey before it says done. + +The agent calls the CLI, reads the library and the cards, and writes the files under `.uxcli/` that a +person then picks, answers or signs. Everything uxcli prints is written for the agent to act on without +interpretation: `what · where · rule · check`, an exit code, a path to the evidence. ## Purpose -uxcli is the place, the rules and the instrument between an agent and an interface: +uxcli gives a coding agent the habits of a designer who did the research, in four pillars: -- **A place** — `.uxcli/`: one file per thing somebody decided, in a shape the machine validates - (`schemas/`) and never edits. -- **Rules** — no commitment, no verdict; the agent proposes and a person signs; nobody edits a - declaration to make a run pass; confidence is a ceiling derived from evidence. -- **An instrument** — `context show` before designing, `run` after, in real Chrome, against the - project's own declarations. +- **Knowledge** — a library of cited rules (lenses by kind of UI, topic pools for colour, writing, + data display and forms) and templates for kinds of product. Every rule names its author, its page, + its exact words, and how it is checked. +- **Process** — research the field (`references/research.md`), write what was learned as insights and + what was not as questions, read `context show`, draw variants to the lens, build the one a person + picks. +- **Evidence and versions** — one shape for what a walk saw (screenshots, the trace, highlighted + problems), proposals for a new version that point at the evidence, versions compared along the flow. +- **Experience validation** — walk the journey in real Chrome and measure what the person goes through: + steps against the shortest path, time a practised user would need, response within a second, data + carried between steps, recovery from an error, consistency between screens. -The order matters. Understanding before journey, journey before run, run before commitment. `uxcli -init` prints the first undone rung, every time. +The centre is the fourth. Technical checks on a page are the floor, not the product. ## Personality -Terse, literal, accountable. A card says what was measured and who signed the rule; it does not -advise, praise, or soften. Silence is a valid output: where nobody committed, uxcli says nothing. -Every claim in the README is one the gate can fail. +Knowledgeable, sourced, plain. uxcli has opinions — it ships a library of them — but every opinion has +a name and a page on it, and says how it can be checked. A card says what was measured and what was +believed, and never dresses one as the other. Silence is a valid output: where nothing was measured, +nothing is claimed. ## Anti-references What uxcli is not, so a feature that drifts there is refused: -- **Not a UX opinion engine.** It generates no insight, no persona, no recommendation. A built-in - table of "best practices" would be a claim with no source and no owner. -- **Not a score.** No 0–100, no letter grade, no "UX health". Seven verdicts, one exit code. +- **Not a UI generator.** It does not compete with the tools that draw screens; it makes whatever draws + them draw from research and checks what was drawn. +- **Not unsourced taste.** A rule nobody can cite does not ship. "Best practice" without an author is a + hypothesis, labelled `folklore`. +- **Not a score.** No 0–100, no letter grade, no "UX health". Measurements with units, findings with + sources, one exit code. +- **Not a substitute for real users.** Walkthroughs and simulated personas find what a careful designer + would find; they are findings, never verdicts, and the card says nobody was watched. - **Not visual regression.** A pixel diff says "different"; different is not wrong. -- **Not a conformance certificate.** Four page probes at exit 0 is a floor, not a WCAG audit. -- **Not a design-critique agent.** Taste is not committed to by anyone, so it is never measured. -- **Not a dashboard first.** A card in the terminal, a packet on disk. Surfaces come after the - numbers do. +- **Not a conformance certificate.** Page checks at exit 0 are a floor, not an audit. ## Design principles -1. **The card is the argument.** Every verdict cites the measurement, the place, the rule and how - to check it by hand. If a surface cannot print those four, it is not done. -2. **Every claim is a pair.** A probe ships a must-fail and a must-pass; a rule about the code is a - test that plants the violation; a skill paragraph has a record of fresh sessions with and without - it. `uxcli gate` runs all of it. -3. **Authored, derived, observed.** A file is one of the five kinds in the README's table, and the - kind decides who may write it and whether git keeps it. The machine never edits an authored file. -4. **Ceilings, not claims.** Strength on a state, confidence on an insight, trust on a project: each - is computed from what is on disk, and an author may only lower it. -5. **Exit 0 is a floor.** The instrument says what it measured and what it did not look at, in the - same breath. +1. **The card is the argument.** Every line cites the measurement or the source, the place, the rule + and how to check it by hand. If a surface cannot print those four, it is not done. +2. **Measured may block; believed may only inform.** A check blocks only when it has a pair (a case it + must fail, a twin it must pass) and a record of running on unseen pages with no false fails. + Everything else is a finding. +3. **Every claim is a pair.** A probe, a schema, a template, a skill paragraph: `uxcli gate` holds a + way to make each one fail on demand. +4. **Sources are fetched, quotes are verbatim.** The library carries words someone actually wrote, on a + page someone can open. An unknown about the user stays a question until a source answers it. +5. **Create, never overwrite.** `init`, `template apply`, `review --write` only ever add files. What a + person wrote is theirs. +6. **Exit 0 is a floor.** The instrument says what it measured and what it did not look at, in the same + breath. ## Accessibility -The instrument's own output is text in a terminal with no colour dependence. The WCAG probes it ships are the four whose -method has a validation record; a probe without one may only say `finding`. +The instrument's own output is text in a terminal with no colour dependence, and the pages it writes +(mockups, map) are measured by its own page checks in the gate. diff --git a/README.md b/README.md index 107c56f..00319c1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ <h1 align="center">UXCLI</h1> <p align="center"> - <strong>Give a coding agent the product context it needs before it designs a screen.<br>Then measure the screen in real Chrome before it says done.</strong> + <strong>Make a coding agent design like a designer who did the research:<br>learn the field, design from cited principles, then walk the journey in real Chrome before it says done.</strong> </p> <p align="center"> @@ -26,22 +26,26 @@ npx -y @junixlabs/uxcli demo ./uxcli-demo ## What it is -The agent that writes an interface is not the authority that declares it correct, and it is not the -source of what the product's users need either. uxcli is the place, the rules and the instrument -between those two facts: - -- **A place** — `.uxcli/` in your repo, with one file per thing somebody decided: who the actor is and - what is still unknown about them, each insight with its evidence and its falsifier, each journey - as states the browser can check, each commitment with a named owner and a written source. -- **Rules** — no commitment, no verdict. The agent proposes; a person signs (or an agent signs on - that person's say-so, and the file says so). Nobody edits a journey or a commitment to make a run - pass. Confidence on an insight is a ceiling derived from its evidence, never the author's word. -- **An instrument** — `uxcli context show` prints what the agent must read before it designs; - `uxcli run` drives real Chrome against the running product, measures every declared state and - signed commitment, and returns the verdict as an exit code with the pixels behind it. - -uxcli writes no claim about the product. Every sentence on a card was authored by the project or -measured by the browser. +A coding agent designs from what it assumes. uxcli gives it what a designer who did the research would +have, and an instrument to prove the result works for the person using it: + +- **Knowledge** — a library the agent can cite: 167 rules from named designers, design systems and + studies, each with its author, page and exact words, packaged by kind of UI (`marketing`, `content`, + `data`, `workspace`, `shop`, `transaction`) and by topic (colour, writing, data display, forms); and + templates for kinds of product (`workspace`, `shop`, `landing`) that name the screens, the lens for + each, the journeys to walk first and the questions research must answer. +- **Process** — research the field from sources, write insights with evidence and open questions as + unknowns, read `uxcli context show` before designing, draw variants to the lens, build the one a + person picks. +- **Evidence and versions** — every walk of a journey leaves `run.json`, screenshots and the trace; + picks and revisions are hashed to the drawing they speak of; `uxcli map` lays the journey out step by + step. +- **Experience validation** — `uxcli run` walks the journey in real Chrome and checks what the person + goes through, not only how the page is built. Checks proven able to fail may block; designer rules + and walkthroughs are reported as findings with their source, never as a pass. + +uxcli writes no claim about your users. Every sentence on a card was authored by the project, quoted +from a cited source, or measured by the browser. ## Install @@ -64,12 +68,14 @@ Node 20+. `npx -y @junixlabs/uxcli <command>` works without installing. ```bash uxcli init --apply --origin=http://localhost:3000 # a rule file, the skill, .uxcli/ with the floor policy +uxcli template show workspace # the kind of product: screens, lens per screen, what to research uxcli context show handle-inbound-lead # read before you design uxcli mockups # the screens drawn as the journey's flow; a person picks uxcli lens show workspace # named designers' viewpoints for this kind of UI uxcli review check # every lens review complete, fresh, not contradicted by a probe uxcli run .uxcli/journeys/handle-inbound-lead.json # measure after uxcli run http://localhost:3000/leads/1 --prove # one screen: five probes, each pass made to earn it +uxcli experience --page # what the person goes through on each journey, pinned on the screenshots uxcli map # the journey as screens: declared, observed, the difference, the verdicts ``` @@ -92,10 +98,12 @@ outlined where the browser found them (the `hooks` toggle shows them on the thum the journey tabs filter the page to what one journey names. A picture someone made of a screen goes in `refs/` under that screen and is shown as a reference, never as a variant. -`lens` is a library of viewpoints from named designers, 123 of them in four schools — usability +`lens` is a library of viewpoints from named designers and design systems, 167 of them: four schools — usability (Nielsen, Norman, Krug, Baymard, Wroblewski, GOV.UK), practitioner craft (Wathan & Schoger, Kennedy), the classic canon (Rams, Vignelli, Müller-Brockmann, Bringhurst, Butterick, Tufte, Gestalt via NN/g) and modern product craft (Rauno Freiberg, Emil Kowalski, Linear, Vercel, Apple, Material, Ström, Brignull) — +and four topic pools drawn from public design systems and WCAG (colour, writing and microcopy, data +display, forms: GOV.UK, USWDS, Carbon, Primer, Polaris, Mailchimp, W3C) — each with its author, work, page and words, packaged by the kind of UI it is read against: `marketing`, `content`, `data`, `workspace`, `shop`, `transaction`. Viewpoints that several schools state as one rule are one line of the checklist. The lenses live inside the skill (`skills/uxcli/lenses/<kind>.md`, installed @@ -185,8 +193,11 @@ uxcli init [dir] [--apply --origin=URL] # where the project stands and the fi uxcli context show [journey] # what to read before designing uxcli mockups [dir] [--viewport=WxH[,WxH…]] # every screen's variants photographed; the picked ones as each journey's flow uxcli lens [show <kind>] # the shipped lenses: named designers' viewpoints by kind of UI +uxcli template [show <id> | apply <id>] # where to start for a kind of product: screens, lenses, journeys, research questions uxcli review <state>/<variant> --lens=<kind> --write # an empty review beside a drawing (a URL with --name reviews a screen) uxcli review check # every review complete, fresh, and not contradicted by a probe +uxcli version [save <journey> <name>] # name a walk so it is kept; experience --journey=<id> --from=<a> --to=<b> compares two +uxcli experience [dir] [--page] # what the person goes through: steps, typing, scrolls, waits, an estimate in seconds; findings pinned on screenshots uxcli map [dir] # the journey map page: MODEL · RUN · DIFF · IMPACT, step by step, with the evidence uxcli run <journey.json> # measure a journey --env --origin --viewport --json --out uxcli run <url> # measure one screen --prove --state=FILE --src=DIR diff --git a/ROADMAP.md b/ROADMAP.md index 85d945a..ec6f2f3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,65 +1,90 @@ # Roadmap -Phases end when their exit criteria pass. No dates. Order reflects what the evidence so far supports, not the original plan: flow probes came first because they are the part no other tool has; the single-screen probes moved ahead of the second flow cut after the first real-project run, where the flow probes were mostly not applicable and the single-screen probes found the defects. +Phases end when their exit criteria pass. No dates. The order follows the four pillars in VISION.md, +and the owner's call of 2026-10-03: build the library first, then the experience metrics that check it. ## Done -- **Prove.** Frozen, hashed definitions run on unseen pages and a seeded flow fixture; agents tested against the verdict format. Records live in the working notes; the README carries no evidence table until each row can name its runner and whether the definitions were frozen before or revised after the pages were seen. -- **Instrument.** `run <url>` with three single-screen probes (2.4.7 pixels by a real Tab press, 1.4.12 by ACT rules, 1.4.3 by a pinned axe-core), every twin reaching `pass`, method status per probe, all three `method-validated` on unseen pages (details below). -- **Flow, first cut.** `run <journey>` with three probes (WCAG 3.3.4, 3.3.7, 3.2.3), process segments with a human `sameProcess` override, the verdict card, `--json`, `--refute`, and `gate` with a falsification pair per probe. - -## Next - - -### Flow, second cut -- Done in part: recall on defects seeded blind by two independent agents (a multi-page checkout, 12 mutants; a same-URL wizard, 8 mutants): every in-scope defect caught after two recorded revisions, 0 false fails, no-verdict counted separately; the key for one wizard mutant is contested and recorded as such. Still open: precision on 20 flows the probes have never seen. -- Done: same-URL wizards (journey `expect`, or the submitted form gone / heading changed); navigation built from buttons and roles; footer and header landmarks as mechanisms; login identity on a password step as the security exception; `back` only as a whole label; reformatted values as a finding. Still open: change controls matched by observed URLs; disabled submit as a checking mechanism; forced clicks for overlay widgets; unlandmarked menus as a lower-provenance fallback. -- Done 2026-09-07: **3.3.1 error identification** shipped with its pair on day one (must-fail: silent rejection; must-pass: message tied by `aria-describedby` and `aria-invalid`), `method-unproven`. Candidate changed from 2.2.1 timing because every page with a form needs it, and the first outside run (a landing page with one form) had nothing else to measure. It plants one error (a required field left empty, submit) so it runs only where the journey opts in, never on a bare URL; `method-unproven` at birth. -- Exit: three numbers published separately: precision, recall, no-verdict rate. - -### Skills -The instrument is used by an agent; the skills are how the agent is told to use it. A skill is a set of obligations on a fresh agent, and it is correct only when its transcript shows the obligations met and its mutant shows them broken. -- `principles` shipped in 0.4.0 without a twin; it gets one here. -- `journey`: from `discover` output to `*.proposed.json` with the questions only the owner can answer (which step commits, `expect` on a same-URL step, values, a test endpoint). Must not set `confirmedBy`, must not place the file among the project's journeys, must not fill hidden or invisible inputs. -- `before-done`: what an agent must do before it may say the UI work is finished: run `uxcli run` on the screens it touched and the journeys it affected, fix or report every `fail`, call a `finding` unverified rather than a pass, say "done" only at exit 0. Named after its trigger, not after a verdict it does not give. -- Done 2026-09-07: `journey`, `before-done` written and tested on 95 fresh sessions with four arms (intact, one paragraph removed, every statement removed, no skill). No paragraph-level twin ever broke; the absence of the skill did: without `principles` 5/5 agents wrote the commitments file themselves; without `journey` 2/5 put the proposal in the confirmed directory; without `before-done` and with no mention of uxcli 4/10 never ran the instrument. Records in `skills/pair.json`. -- Learned: the card already carries "a finding is not a pass" and "a fail blocks done" (0 of 45 sessions with the instrument running said otherwise, skill or not). A skill earns its place only for what the instrument cannot say by itself: start me, and keep the proposal out of the confirmed directory. -- Exit met 2026-09-07 with the twin redefined as the absence of the skill (the paragraph twins measured redundancy, recorded as such). VISION "Shape" names two skills; a third is the owner's change to make. - -### Falsify on the target -A falsification pair proves a probe can fail on a fixture. Nothing proves it can fail on this project. An all-green run must be able to show its counterfactual. -- Done 2026-09-07 for the three page probes: `run <url> --prove`, reached decided by computed style before the re-measure, gate requires `would fail` on every must-pass twin; the first real page gave `would fail` on focus-visible. Flow probes not yet (they need the overlay mechanism, not a live-page mutation). -- `run --prove`: for each probe that passed, plant one defect that reaches the very controls it measured (a focus rule that changes their computed style, a colour override on the cited text group, a locked `letter-spacing`; for a journey, a dropped review value or a reordered navigation item), re-measure the same controls, print `pass · would fail on <mutation>` or `pass · could not be made to fail`, which is a warning. -- Definition first: what "the mutation reaches the measured element" means is written and committed before any code. -- Exit: on the twins and on one real project every pass carries its counterfactual line. - -### Commit -The project's own commitments become thresholds. -- Done: `uxcli.commitments.json` with owner and source per entry; `sheet` measures `contrast` commitments on declared tokens (aliases resolved, theme-ambiguous tokens `unmeasurable`); falsification pair in the gate. -- Done: `principles`, a skill that drafts the file as a proposal with trade-offs; the human fills owner and source and commits it. The agent may propose, it may not commit. -- Done: `discover <repo|url>` writes journey candidates as proposals; `run` refuses a proposal until a human sets `confirmedBy`. -- Done: `diff a b --gate` over two saved runs; only `fail` blocks. -- Journey authoring beyond proposals: the `journey` skill (see Skills). -- `init`: copy the shipped skills into the project's `.claude/skills/`, create `.uxcli/`, print the CI step. Nothing else: no commitments file, no journey, no edits to existing files. -- Exit: one real project runs `sheet` or `diff --gate` in CI on its own commitments. Not yet: the first project's commitments file has to be written by its owner. - -### Release -- Done: on npm as `@junixlabs/uxcli`; CI runs `gate` on every push; `publish.yml` publishes tags `v*` through npm trusted publishing, with provenance. -- Done: 0.3.0 = Instrument exit (three page probes `method-validated`); 0.4.0 = Commit tooling (`sheet`, `diff --gate`, `discover`, `principles`). The Commit exit itself is still open above. -- Done: 0.5.0 = the `journey` and `before-done` skills with their records, `init`, flow probe 3.3.1 with its pair, `run --prove` for the page probes, the `discover` dedup fix. -- Done: 0.5.1 = `--refute` explicit about what it spawns and what it costs; `run.json` in every run directory; card footer and two GitHub issue forms (wrong or missed verdict; a flow for the unseen list). Feedback from projects enters here and nowhere else. -- Done: 0.5.3 = `text-overlap`, the first probe with provenance `opinion` (text painted over text at rest), written from the first feedback issue (#1: a step counter painted over a command name on the project's own landing draft, missed by all three page probes). Ships `method-unproven` with its pair; needs 20 unseen pages. Also: a status line on stderr before a page run when attached to a terminal. -- 0.6.0 when the flow probes flip to `method-validated` (20 unseen flows, the Gate 0 ruling); 0.5.x for the Commit exit and `--prove` on flow probes. -- CI action running `run` against a live fixture; the planted-verdict test repeated on a second model. -- Exit: a stranger installs it and gets a first verdict in ten minutes without reading the source. - -## Instrument, record -Single-screen probes into the package with the same contract as the flow probes: spec, code, hashed must-fail and must-pass fixtures. -- Done: `run <url>` with focus-visible (2.4.7) and text-spacing (1.4.12, ACT 24afc2/9e45ec/78fd32), both carried from Prove with recorded revisions, and contrast (1.4.3) delegated to a pinned axe-core; `--state` for signed-in pages; third-party subtrees excluded; bot challenges and load errors reported as `unmeasurable` with the reason. -- Done: `gate` covers every probe; the must-pass twin of each page probe has to reach `pass`, never `not-applicable`; non-pass card lines carry the reason. -- Done: every twin, flow and page, must reach `pass`; method status per probe (`method-validated` needs a recorded unseen run with the packaged code, 0 false fails, and a recall record); unproven probes report `finding`. -- Done: focus-visible measures rendered pixels per control after one real Tab press; what a crop cannot show (off-viewport, covered, self-changing, ring drawn elsewhere) is reported per control as not measured, never as fail. Each false-fail class found on unseen pages is a recorded revision in the spec. -- Exit met 2026-09-06: 20 pages the probes had never seen (Tranco order, list hashed, definitions committed at `8dde0e1` before the draw), re-run with the packaged code: focus-visible 62 failing controls on 7 pages, 58 re-measured by a real Tab press and a whole-viewport diff, 4 by the viewport diff alone (the check could not land Tab on them), 0 false fails; text-spacing and contrast 0 false fails over 80 unseen pages. It took four lists: the first three each found a false-fail class the probe's own measurement could not see (eight recorded revisions, v3.1 to v3.8). All three page probes are `method-validated`. +- **Page checks.** `run <url>` with focus-visible (2.4.7), text-spacing (1.4.12) and contrast (1.4.3) + `method-validated` on 20–80 unseen pages with 0 false fails; text-overlap and nesting as findings; + `--prove` plants each check's own defect so every pass earns it. +- **Flow checks, first cut.** `run <journey>` with 3.3.1, 3.3.4, 3.3.7 and 3.2.3, each with its pair; + method-unproven, so they report findings. +- **Context before design.** Understanding (actors, insights with evidence and a falsifier), journeys + as states the browser can check, `context show`. Measured: 0/10 sessions from a ticket alone, 10/10 + with the card. +- **Drawing and picking.** `mockups`: variants per state photographed, one decision per screen, picks + and revisions hashed to the drawing; `map`: the journey as screens, model, run, diff and impact. +- **Lenses.** 123 viewpoints from four schools of designers, packaged by kind of UI; `review` and + `review check`, which refuses a review a probe contradicts. +- **One skill, three ways in.** `doctor`, `demo`, `guide`; the skill installs into Claude Code, Codex, + opencode and Cursor. + +## 1. Knowledge — the library + +- Done 2026-10-03: four topic pools from public design systems and WCAG — colour (10), writing and + microcopy (10), data display (12), forms (12) — every quote fetched from the source repository and + checked verbatim; folded into the six lenses. +- Done 2026-10-03: templates for three kinds of product — `workspace`, `shop`, `landing` — with + screens and their lens, journeys to walk with what to watch, research questions and sources; + `template apply` writes the questions as an actor's unknowns, creating only. +- Done 2026-10-03: `references/research.md`, the order in which the field is researched and how + findings are written down. +- Next: templates for `content` (docs, help centres) and `mobile-app`; a topic pool for navigation and + information architecture; a topic pool for motion and feedback. +- Next: `context show` names the template and the lens for each screen of the journey. +- Exit: an agent given only a ticket and the skill picks the right template and lens, and its drawing's + review cites topic-pool rules, in 8 of 10 fresh sessions. + +## 2. Experience validation — the centre + +Measured from the trace of a real walk in Chrome, each with a must-fail and a must-pass journey: + +- **Steps against the shortest path**: actions taken, clicks, characters typed, back-tracks, dead ends. +- **Keystroke-level estimate**: the time a practised user would need for the journey (Card, Moran & + Newell's operators), so two versions can be compared in seconds rather than impressions. +- **Response**: time from each action to the first visible change; a progress signal past one second. +- **Continuity**: data entered on one step present on the next; nothing asked twice (3.3.7 generalised). +- **Recovery**: an error planted on purpose — a message, at the field, with the other answers kept. +- **Consistency**: navigation, terms and the primary action in the same place across the journey's + screens (3.2.3 generalised). + +- Done 2026-10-03: `uxcli experience` reports steps, typing, scrolls, settle time in Nielsen's bands + and a keystroke-level estimate per workflow; a wait past 1 s, a control below the fold, an answer + typed twice, a step that did not arrive; after an error answer, a typed answer the page cleared + (passwords excepted) and an error nothing announced; a navigation landmark whose items change order + within a workflow. `--page` pins each on the step's screenshot. All findings; none validated yet. + +Every finding is pinned on the step's screenshot. Exit: each metric has its pair in the gate, and on +the CRM example a planted extra step, a missing progress signal and a lost value are each caught. + +## 3. Evidence and versions + +- A trace schema (the actions, their targets, timings and the state after each) and an evidence schema + (a screenshot, a region, what was found there, which rule or metric). +- A version of a journey: its screens, metrics and findings at one build. A proposal for a new version + names the evidence that caused it. +- `map` compares two versions step by step: what changed on screen, and what the metrics did. +- Done 2026-10-03: `uxcli version save <journey> <name>` names a walk (one file per version, never + rewritten; its run kept against pruning); `experience --journey --from --to [--page]` compares two + versions step by step with both pictures side by side; proposals take `kind: redesign`, with + `evidence[]` citing the walk; `references/versions.md` is the loop. The trace and evidence shapes are + the run packet and the experience report; a schema of their own is next. +- Exit: on the CRM example, v1 and v2 side by side, with the metrics that moved. + +## 4. Walkthrough and personas + +- Cognitive walkthrough at each step: the four questions answered with the screenshot, as findings. +- Personas from the project's own understanding walk the journey and record where they hesitate. +- Exit: agreement with a human designer measured on a sample and published with its limits. Until + then, findings only. + +## The decisive experiment + +Same ticket, fresh sessions, agent with uxcli against agent without, scored on the walk (the metrics +above) and by blind pairwise preference from designers. The repository already has the harness +(`experiments/understanding-before-design`). ## Not planned -Scores. Summaries. Conformance claims. Design critique. An MCP tool schema for the common path. + +Scores. Summaries that hide their sources. Conformance claims. A UI generator. A rule nobody can cite. diff --git a/VISION.md b/VISION.md index 021b9dc..334f020 100644 --- a/VISION.md +++ b/VISION.md @@ -1,72 +1,87 @@ # UXCLI -**Every verdict cites the commitment it enforces: W3C's, yours, or none.** -**Where no one has committed, UXCLI says nothing.** - -## Design should be executable. - -Software has machines that refuse a build that breaks a contract. Nothing refuses a build that breaks a journey. A journey map dies at handoff. A token becomes a suggestion. A principle becomes a thread. What ships is whatever survived, and no instrument can say how far it drifted from what was decided. - -UXCLI is that instrument. It measures the running product against the commitments that produced it: the journeys that matter, the tokens that define the system, the principles that govern behavior. It drives a real browser. It follows flows. It measures change over time. It locates drift between builds to the element and the file. It sees defects that exist between screens, not only inside them. - -This matters now because the author of the interface is changing. Agents produce more frontend than humans can inspect. The agent that writes the interface cannot be the authority that declares it correct. It needs perception, not another opinion. - -A commitment becomes a measurement. A measurement produces evidence. Evidence earns a verdict. - -**Your design, executable.** - -## The next layer is not generation. It is perception. - -axe answers *is this page broken?* Dembrandt answers *did the tokens drift?* Nothing answers *does the flow the user came for still complete?* The difference is the input. Those tools read a page. UXCLI reads a page and the commitment, and the commitment is a journey. That input makes a new class of defect measurable. - -- **Between screens.** Data entered on step one, absent from the review on step three. Navigation that reorders itself between pages. No page-level tool can see this, because no page-level tool remembers. -- **Across time.** A toast that lives 800 ms when the principle says four seconds. Motion is data, not noise to freeze. -- **Between builds.** Drift, reported at the element and the file that caused it. Never as a score. -- **At five scales.** One element. One screen. One flow. The whole design system. The delta between two builds. One instrument, five zoom levels. - -## Who runs it - -The human operating a coding agent. The pain is specific: the agent reports success with proof of failure in hand. UXCLI is the instrument an agent runs before it is allowed to say done. - -Commitments are written by a human, before the build, and versioned. The agent may propose one. It may not commit it. The party being measured never authors the measure in the same run. - -Developers get the same instrument as a pre-merge gate: findings that need no re-checking before fixing, and a `fail` that blocks. - -## Why it stays on - -A review tool that lies three times is disabled forever. UXCLI is built around one rule and its consequences. - -**No commitment, no verdict.** A commitment is a threshold with a named owner and a written source. W3C is a named owner, so WCAG runs by default. Nobody signed taste, so taste never runs. - -- Every finding names its provenance: `spec`, `project`, or `opinion`. Opinion without a written principle stays silent. -- Every probe declares its method. An unproven method can raise a `finding`, never a `fail`. -- No commitment yields `not-committed`, not `fail`. Cannot measure yields `unmeasurable`, never `pass`. -- Nine verdicts. One blocks a merge. -- The opening rule set contains only probes proven not to false-positive, and at least one of them is a flow rule. - -A tool with no opinions of its own has nothing to be wrong about. - -**UXCLI wins by saying less, and being right.** - -## Shape - -Two skills write commitments: `journey`, `principles`. Seven commands measure: `init`, `run`, `sheet`, `query`, `scan`, `diff --gate`, `why`. Everything that carries a name carries `id · why · correct-when · applies-when`. - -UXCLI does not score design. It does not certify taste. It does not replace visual regression. It does not turn uncertainty into green checks. +**A coding agent should design like a designer who did the research, and prove the experience works +by walking it.** + +## The agent is now the designer + +Most new interface code is written by coding agents. They are fast and fluent, and they design from +what they assume: about the field they are building for, about the people who will use it, about what +good looks like. The result is plausible and generic. It uses the database's words instead of the +user's, and it is declared done by the same agent that made it, after a glance at a screenshot. + +A human designer does three things an agent skips. They **learn the field** before they draw — its +vocabulary, its rules, what its people do all day and what goes wrong for them. They **design from +principles somebody tested** — not taste, but what research, design systems and decades of practice +have shown to work for this kind of screen. And they **watch someone use it** — walk the journey, see +where people hesitate, lose their place, or have to type something twice. + +uxcli gives an agent those three habits, as files it reads and an instrument it runs. + +## Four pillars + +**1. Knowledge — a library the agent can cite.** Rules from named designers, design systems and +studies, each with its author, its page and its exact words, packaged by the kind of UI it applies to +(marketing, content, data, workspace, shop, transaction) and by topic (colour, writing, data display, +forms). Templates for kinds of product — a B2B workspace, a shop, a product site — that say which +screens such products have, which lens each is read against, which journeys to walk first, and what +research has to answer. A rule without a source does not ship; every rule says how it is checked. + +**2. Process — research, then design, then build.** The agent starts from a user story or a journey, +researches the field from sources in a fixed order of trust, writes what it learned as insights with +evidence and what it could not learn as open questions, draws two or three variants to the lens for +each screen, and builds the one a person picks. `context show` prints all of it before the agent draws. + +**3. Evidence and versions — one shape for what was seen.** Every walk of a journey leaves screenshots, +the trace of what was done, and the places that went wrong, highlighted on the frame. A proposal for a +new version of a screen points at the evidence that caused it. Versions are kept and compared along +the flow, step by step, so "better" is something you can see. + +**4. Experience validation — the centre.** Checking that a page is technically sound is the easy part +and is solved elsewhere. uxcli's question is whether the person can do what they came to do: how many +steps it takes against the shortest path, how long a practised user would need, whether every action +answers within a second, whether data typed on one step is still there on the next, whether an error +can be recovered without starting over, whether the screens agree with each other. These come from the +trace of a real walk in real Chrome, with methods HCI has used for decades — task efficiency, the +keystroke-level model, Nielsen's response-time limits, cognitive walkthrough. + +## Why it can be trusted + +A review tool that is wrong three times is switched off forever. So uxcli keeps a line between what it +measured and what it believes. + +- **Measured, and proven able to fail, may block.** A check ships with a case where it must fail and a + twin where it must pass, and blocks only after it has run on pages it was never tuned on with no + false fails. `uxcli gate` holds every such pair on every push. +- **Believed, may only inform.** A designer's rule, a walkthrough answer, a simulated persona's + hesitation: each is reported as a finding with its source, never as a pass and never as a block. +- **The agent does not get to say done with a failure in hand.** Exit 0 means the checks that ran found + no fail. The card says what ran and what it did not look at. +- **Nothing unsourced is presented as knowledge.** A quote is fetched and verbatim, or it is not a quote. + An unknown about the user stays a question until a source answers it. + +## Who uses it + +The person operating a coding agent on a product with an interface, and the agent itself. The agent +reads the library and the context before it designs, walks the journey after it builds, and reports +what the walk showed. The person picks between variants, answers the questions research could not, +and decides what the product commits to. + +## What it is not + +A UI generator — there are many, and uxcli works beside any of them. A score — there is no 0–100 for an +experience. A replacement for watching real people — it finds what a careful designer would find +walking the flow, and says plainly that it did not watch anyone. ## Where it stands | Axis | Evidence | Verdict | |---|---|---| -| Method | focus-visible 7/7 W3C ACT, 22 GOV.UK pages 0 false fails; text-spacing 62/62; axe contrast 0 FP. Definitions frozen and hashed, then 20 unseen pages on 5 sites: 0 false fails. Three independent detectors and chrome-devtools-mcp #86 converged on the same computed-style approach. | Confirmed | -| The flow claim | The gap is real: axe has no rules for 3.3.4 / 3.3.7 / 3.2.3; Evinced and Lighthouse aggregate per page. Three flow probes written from the WCAG text before any fixture existed. Seeded fixture: 3/3 caught, silent on the clean twin. 10 flows on 4 real apps the probes had never seen, definitions frozen: 0 false fails; one real 3.3.7 defect found by hand that the probe did not see. | Measured on fixtures. Unproven at scale. | -| Understanding before design | Same ticket, same product with the lead page removed, ten fresh Sonnet sessions per arm, scored by `uxcli run` on the page each wrote (exit 0, C-001 pass, every measured state held at 390×844). Ticket alone: 0 of 10, and no session put a single `data-uxcli` hook on the page. Ticket + the journey file: 5 of 10. Ticket + the card `uxcli context show` prints: 10 of 10. A project with the skill installed and nothing inline: 10 of 10, and 9 of 10 ran `uxcli run` unprompted. `experiments/understanding-before-design/results/summary.md`, 2026-09-28. | Measured on one model, one fixture, one ticket. The card, not the raw journey, is what moves the number. | -| Adoption | The strongest community pain is agents that report success with proof of failure in hand. 40 fresh Sonnet sessions on the seeded fixture, 20 of them running the tool themselves: 0 reported done while a check still failed, and none treated `unmeasurable` as a pass. Agents given the card edited only the cited file in 19 of 20 sessions; agents given the full JSON packet, in 7 of 20. Given a planted wrong `fail`, 2 of 10 agents without the tool rewrote the site to satisfy it; 0 of 10 with the tool in hand did, all re-ran it and disputed. Re-measured 2026-09-28 with the one shipped skill: 10 of 10 fresh sessions ran uxcli, 0 of 10 said done while the page they left measured a fail, 0 of 10 edited a declaration under `.uxcli/`. | Measured on one model and one fixture. The resistance comes from re-running the measurement, not from the report's wording. | - -Next, in order: 20 flows on apps the probes have never seen; recall from defects seeded into real pages by someone who does not tune the probes; a second model on the planted-verdict test. +| Page checks | focus-visible 7/7 W3C ACT, 22 GOV.UK pages 0 false fails; text-spacing 62/62; axe contrast 0 FP. Definitions frozen and hashed, then 20 unseen pages on 5 sites: 0 false fails. | Confirmed for three checks | +| Flow checks | Three flow probes written from the WCAG text before any fixture existed. Seeded fixture: 3/3 caught, silent on the clean twin. 10 flows on 4 real apps never seen: 0 false fails; one real 3.3.7 defect found by hand that the probe did not see. | Measured on fixtures; unproven at scale | +| Context before design | Same ticket, ten fresh sessions per arm, scored by `uxcli run` on the page each wrote. Ticket alone: 0 of 10. Ticket + the journey file: 5 of 10. Ticket + `uxcli context show`: 10 of 10. The installed skill with nothing inline: 10 of 10. 2026-09-28. | One model, one fixture, one ticket | +| Saying done | 40 fresh sessions on a seeded fixture, 20 running the tool: 0 said done while a check still failed. Re-measured with the shipped skill: 10 of 10 ran uxcli, 0 of 10 said done holding a fail. | One model, one fixture | +| Library | Lenses: 123 viewpoints from four schools of designers; topic pools for colour, writing, data display and forms; templates for three kinds of product. Every quote fetched and verbatim. | Built; whether it changes what agents draw is unmeasured | +| Experience metrics | Not built yet. | The next experiment: agent with uxcli against agent without, scored on the walk | **The part worth building is the part not yet proven.** - -## Sources - -developersdigest.tech/blog/ai-design-slop-and-how-to-spot-it · impeccable.style/slop · solodesign.cc/blog/ai-design-slop-the-tells · github.com/ChromeDevTools/chrome-devtools-mcp/issues/86 · github.com/anthropics/claude-code/issues/38948 · news.ycombinator.com/item?id=46594200 · arxiv.org/abs/2503.15885 · arxiv.org/abs/2502.10884 · dequeuniversity.com/rules/axe/4.10 · github.com/dembrandt/dembrandt diff --git a/bin/uxcli.js b/bin/uxcli.js index 7d3c939..eb93fc3 100755 --- a/bin/uxcli.js +++ b/bin/uxcli.js @@ -43,6 +43,9 @@ const usage = `usage: --apply creates the shipped skill in dir/.claude/skills/uxcli/, dir/.claude/rules/uxcli.md (read at the start of every session), and dir/.uxcli/. It only ever creates: nothing is edited, overwritten or appended to uxcli gate run every probe's falsification pair; exit 1 unless all hold uxcli lens [show <kind>] the shipped lenses — named designers' viewpoints by kind of UI (marketing, content, data, workspace, shop, transaction) + uxcli experience [dir|run] [--page] [--json] what the person goes through on each journey's last walk: steps, clicks, typing, scrolls, waits, a keystroke-level estimate, and what changed since the walk before; findings only, never a fail + uxcli version [save <journey> <name> [--note=…] [--run=DIR] [--proposal=proposals/P-x.json]] name a walk of a journey so it is kept and can be compared: uxcli experience --journey=<id> --from=<name> --to=<name> [--page] + uxcli template [show <id> | apply <id> [dir]] where to start for a kind of product (workspace, shop, landing): screens and their lens, journeys to walk, what to research; apply writes the actor questions as unknowns, creating only uxcli review <state>/<variant> --lens=<kind> --write an empty review beside the drawing; a URL with --name=<n> reviews a screen uxcli review check every review complete, fresh against its drawing, and not contradicted by a probe uxcli why <rule> print a probe's definition (e.g. why 3.3.7, why redundant-entry, why 2.4.7, why text-overlap) @@ -136,6 +139,37 @@ try { console.log(flags.has('--json') ? JSON.stringify(l, null, 1) : L.lensShowCard(l, proj)); } else console.log(flags.has('--json') ? JSON.stringify({ lenses: lib.lenses.map(l => ({ id: l.id, name: l.name, when: l.when, viewpoints: l.viewpoints.length, on: !proj.off.includes(l.id) })), off: proj.off, problems: [...lib.problems, ...proj.problems] }, null, 1) : L.lensListCard(lib, proj)); process.exit(lib.problems.length || proj.problems.length ? 1 : 0); + } else if (cmd === 'experience') { + // What the person goes through on each journey's last walk: steps, typing, scrolls, waits, an estimate in seconds. + const E = await import('../src/experience.js'); let list; + if (opt('from') && opt('to')) { if (!opt('journey')) { console.error('--from and --to compare two versions of one journey: add --journey=<id>'); process.exit(1); } const c = await E.versionComparison(args[0] || '.', opt('journey'), opt('from'), opt('to')); if (c.problems) { console.error('uxcli: ' + c.problems.join('\n ')); process.exit(1); } list = [c.entry]; } + else list = E.experiences(args[0] || '.'); + if (flags.has('--json')) console.log(JSON.stringify(list.map(x => ({ dir: x.dir, ...x.report, ...(x.change && { change: x.change }) })), null, 1)); + else console.log(E.experienceCard(list)); + if (flags.has('--page')) { const f = E.experiencePage(list, opt('out') || E.pageFile(args[0] && fs.existsSync(path.join(args[0], '.uxcli')) ? args[0] : '.')); console.log(` page ${path.relative(process.cwd(), f)}`); } + process.exit(0); + } else if (cmd === 'version') { + // Named walks of a journey: kept against pruning, compared with experience --from --to. + const V = await import('../src/version.js'); const root = path.resolve(opt('src') || '.'); + if (args[0] === 'save' && args[1] && args[2]) { + let by = { type: 'person', ref: 'unknown' }; + try { const { execFileSync } = await import('node:child_process'); const n = execFileSync('git', ['config', 'user.name'], { cwd: root, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); if (n) by = { type: 'person', ref: n }; } catch {} + if (opt('as')) by = { type: 'agent', ref: `${opt('as')}${opt('for') ? ` for ${opt('for')}` : ''}` }; + const r = V.saveVersion(root, args[1], args[2], { run: opt('run'), note: opt('note'), by, proposal: opt('proposal') }); + if (r.problems) { console.error('uxcli: ' + r.problems.join('\n ')); process.exit(1); } + console.log(`wrote ${path.relative(process.cwd(), r.file)} · ${r.value.journey}/${r.value.name} is ${r.value.run}, kept against pruning`); + process.exit(0); + } + console.log(flags.has('--json') ? JSON.stringify(V.versions(root), null, 1) : V.versionListCard(root)); + process.exit(V.versions(root).some(v => !v.value) ? 1 : 0); + } else if (cmd === 'template') { + // Where to start for a kind of product: screens and their lens, journeys to walk, what to research. + const T = await import('../src/template.js'); const lib = T.templates(); + const pick = id => { const t = lib.list.find(x => x.id === id); if (!t) { console.error(`no template ${id}; have: ${lib.list.map(x => x.id).join(', ')}`); process.exit(1); } return t; }; + if (args[0] === 'show' && args[1]) { const t = pick(args[1]); console.log(flags.has('--json') ? JSON.stringify(t, null, 1) : T.templateShowCard(t)); } + else if (args[0] === 'apply' && args[1]) { const t = pick(args[1]); const r = T.applyTemplate(args[2] || '.', t); console.log(flags.has('--json') ? JSON.stringify(r, null, 1) : T.applyCard(r, t)); } + else console.log(flags.has('--json') ? JSON.stringify({ templates: lib.list.map(t => ({ id: t.id, name: t.name, when: t.when })), problems: lib.problems }, null, 1) : T.templateListCard(lib)); + process.exit(lib.problems.length ? 1 : 0); } else if (cmd === 'review' && args[0] === 'check') { const L = await import('../src/lens.js'); const J = await import('../src/journey.js'); const root = J.findRoot(path.resolve(opt('src') || '.')); if (!root) { console.error('no .uxcli/policy/policy.json here or above'); process.exit(1); } diff --git a/docs/lenses/README.md b/docs/lenses/README.md index b8c6321..2795f2d 100644 --- a/docs/lenses/README.md +++ b/docs/lenses/README.md @@ -167,3 +167,27 @@ than the drawing, and not say `holds` where a probe the viewpoint names counted Deviation from recommendation B: lenses are on by default (the owner's pick) rather than adopted. The VISION rule still holds in the part that matters: a lens never produces a verdict; a review is shown as the reviewer's claim, and only a probe with its own pair (`page.nesting`) measures anything. + +## Topic pools — 2026-10-03 + +The four schools are designers. The four topic pools are what a team reaches for when the question is +narrower than a school: which colour, which words, how to lay out a table, how to ask a question. Each +was researched by one agent with the same rule as the schools — every quote fetched in the session and +matched verbatim — from the source repositories of public design systems and the W3C, because their +websites were not reachable from the research sandbox. Each note says which raw file a quote was checked +against and that the public URL was derived from the repository's routing. + +| pool | file | viewpoints | sources | +|---|---|---|---| +| colour | [`color.md`](color.md) | 10 | W3C Understanding 1.4.1 and 1.4.11, GOV.UK, USWDS, Carbon, Primer, Polaris | +| writing and microcopy | [`writing.md`](writing.md) | 10 | GOV.UK, Carbon, Primer, Polaris, Mailchimp | +| data display | [`data-display.md`](data-display.md) | 12 | Primer, GOV.UK, USWDS, Carbon, Polaris, W3C WAI tables tutorial | +| forms and input | [`forms.md`](forms.md) | 12 | GOV.UK, USWDS, Primer, W3C Understanding 1.3.5 | + +Where sources disagree the pool records both: GOV.UK validates on submit while +`usability.inline-validation-after-leaving-field` validates on blur, and the forms check tests only what +all agree on (not while typing); GOV.UK forbids asterisks while USWDS uses them, and the check tests only +"(optional)" in words; Primer colours its primary button with its success green, recorded as an exception +to `color.one-action-colour-apart-from-status`. Numbers a source does not give (a reading grade, a +120-character hint, a six-word empty state) are labelled in the viewpoint's exceptions as uxcli's starting +point. `build-lenses.mjs` folds the pools into the six lenses the same way as the schools. diff --git a/docs/lenses/build-lenses.mjs b/docs/lenses/build-lenses.mjs index 3b895e4..2c8f470 100644 --- a/docs/lenses/build-lenses.mjs +++ b/docs/lenses/build-lenses.mjs @@ -2,7 +2,7 @@ // viewpoints that state the same rule (agrees links) folded into one pick whose note names the others. import fs from 'node:fs'; import path from 'node:path'; const LIB = new URL('../../skills/uxcli/lenses', import.meta.url).pathname; -const SCHOOLS = ['usability', 'craft', 'canon', 'modern']; +const SCHOOLS = ['usability', 'craft', 'canon', 'modern', 'color', 'writing', 'data-display', 'forms']; const PROBES = { 'page.nesting': ['craft.fewer-borders', 'canon.tufte-one-plus-one-equals-three', 'canon.gestalt-common-region', 'modern.fewer-borders-more-space'] }; const pools = SCHOOLS.map(s => JSON.parse(fs.readFileSync(path.join(LIB, 'viewpoints', `${s}.json`), 'utf8'))); // the probe a viewpoint is counted by, written into the pool itself diff --git a/docs/lenses/color.md b/docs/lenses/color.md new file mode 100644 index 0000000..d76eb5b --- /dev/null +++ b/docs/lenses/color.md @@ -0,0 +1,119 @@ +# Lens research — colour + +Researched 2026-10-03 for the uxcli "lenses" pool `color` (`skills/uxcli/lenses/viewpoints/color.json`). This cluster covers colour as the open-source design systems and the W3C state it: colour never as the only signal, status colours that keep their meaning, tokens instead of hex, a few families in proportion, one action colour, 3:1 for controls and focus indicators, light/dark modes, and colour in charts. + +**How the text was fetched.** The public design-system sites are blocked to WebFetch in this environment, and the GitHub API returned "access not enabled" for these repositories. Every source below was therefore read as the raw Markdown/MDX/HTML that the public page is built from, fetched in-session with `curl https://raw.githubusercontent.com/<owner>/<repo>/main/<path>` (file paths found with `git clone --depth 1 --filter=blob:none --no-checkout` and `git ls-tree`). The `url` in each viewpoint is the public page built from that file; the raw file is named in each section. + +**How quotes were verified.** Each quote was checked with `grep -F` against the fetched file. Where the source text wraps across lines (Carbon MDX, WCAG HTML) or carries inline markup (`<a>`, `<em>` in WCAG), the check ran on the file with tags stripped and whitespace collapsed (`tr -s ' \n\t' ' ' | sed 's/<[^>]*>//g'`), which is how the sentence reads on the rendered page. No quote was altered; none needed paraphrase to fit. + +Existing viewpoints this pool overlaps and names in `agrees`: `craft.wcag-contrast-and-dont-rely-on-colour-alone`, `modern.contrast-and-not-colour-alone`, `craft.black-and-white-first-limit-hues`, `canon.gestalt-similarity`, `canon.tufte-graphical-integrity`. Text contrast (WCAG 1.4.3) is already a validated probe (`page.contrast`) and is not repeated here; focus *presence* (2.4.7) is the probe `page.focus-visible`, and the focus viewpoint below covers only the contrast of the indicator. + +## Sources fetched + +| source | author | public URL | raw file verified against | +|---|---|---|---| +| Understanding SC 1.4.1 Use of Color | W3C AG WG | https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html | `w3c/wcag` `understanding/20/use-of-color.html` | +| Understanding SC 1.4.11 Non-text Contrast | W3C AG WG | https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html | `w3c/wcag` `understanding/21/non-text-contrast.html` | +| Styles: Colour | GOV.UK Design System | https://design-system.service.gov.uk/styles/colour/ | `alphagov/govuk-design-system` `src/styles/colour/index.md` | +| Color overview | USWDS | https://designsystem.digital.gov/design-tokens/color/overview/ | `uswds/uswds-site` `pages/design-tokens/color/overview.md` | +| Theme color tokens | USWDS | https://designsystem.digital.gov/design-tokens/color/theme-tokens/ | `uswds/uswds-site` `pages/design-tokens/color/theme-tokens.md` | +| State color tokens | USWDS | https://designsystem.digital.gov/design-tokens/color/state-tokens/ | `uswds/uswds-site` `pages/design-tokens/color/state-tokens.md` | +| Data visualizations | USWDS | https://designsystem.digital.gov/components/data-visualizations/ | `uswds/uswds-site` `_components/data-visualizations/data-visualizations.md` | +| Color overview | IBM Carbon | https://carbondesignsystem.com/elements/color/overview/ | `carbon-design-system/carbon-website` `src/pages/elements/color/overview.mdx` | +| Color usage | IBM Carbon | https://carbondesignsystem.com/elements/color/usage/ | `.../src/pages/elements/color/usage.mdx` | +| Accessibility: Color | IBM Carbon | https://carbondesignsystem.com/guidelines/accessibility/color/ | `.../src/pages/guidelines/accessibility/color.mdx` | +| Data visualization: Color palettes | IBM Carbon | https://carbondesignsystem.com/data-visualization/color-palettes/ | `.../src/pages/data-visualization/color-palettes/index.mdx` | +| UI color system | GitHub Primer | https://primer.style/foundations/color/overview | `primer/design` `content/foundations/color/overview.mdx` | +| Color: Accessibility | GitHub Primer | https://primer.style/foundations/color/accessibility | `primer/design` `content/foundations/color/accessibility.mdx` | +| Color considerations | GitHub Primer | https://primer.style/guides/accessibility/color-considerations | `primer/design` `content/guides/accessibility/color-considerations.mdx` | +| Data visualization | GitHub Primer | https://primer.style/ui-patterns/data-visualization | `primer/design` `content/ui-patterns/data-visualization.mdx` | +| Palettes and roles | Shopify Polaris | https://polaris.shopify.com/design/colors/palettes-and-roles | `Shopify/polaris` `polaris.shopify.com/content/design/colors/palettes-and-roles.mdx` | +| Using color | Shopify Polaris | https://polaris.shopify.com/design/colors/using-color | `Shopify/polaris` `polaris.shopify.com/content/design/colors/using-color.mdx` | +| Data visualizations | Shopify Polaris | https://polaris.shopify.com/design/data-visualizations | `Shopify/polaris` `polaris.shopify.com/content/design/data-visualizations.mdx` | + +Primer's public URLs are the ones the Primer files themselves link to (`color-considerations.mdx` links `https://primer.style/foundations/color/overview` and `.../color/accessibility`); the site may have since moved under `primer.style/product/`. Not used: Atlassian and Material, whose colour guidance was not looked for on GitHub in this session. + +## Viewpoints + +### never-the-only-signal +- **Claim:** "Use information in addition to color, such as shape or text, to convey meaning." (WCAG 1.4.1, "In brief: What to do") +- **Source:** W3C, https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html — raw `understanding/20/use-of-color.html`, `grep -F` exact on one line. +- **Corroboration (all grep-verified):** USWDS overview: "Color should only be used as progressive enhancement — if color is the only signal, that signal won’t get through as intended to everyone." Carbon accessibility: "Don't rely on color alone to convey meaning. This includes conveying information, indicating an action, prompting the user for a response, or distinguishing one visual element from another." Primer accessibility: "To make sure everyone can understand and use your UI you should show state with more than a change in color." Primer color considerations, validation state: "Validation state **must not** rely on color as the sole way of determining if an input is valid." (bold markup is in the source). +- **Exceptions (source text):** WCAG: hues that also differ in lightness at 3:1 or more count as a second cue, "However, if content relies on the user's ability to accurately perceive or differentiate a particular color an additional visual indicator will be required regardless of the contrast ratio between those colors"; visited-link colour is not an author responsibility. Primer: "Unless color is only used as a visual \"flourish\"". +- **Measurable?** Yes as a count: greyscale the render and count state pairs that become indistinguishable (hue-only difference under 3:1 with no text/icon/shape difference). Must-fail/must-pass are in the JSON. +- **Evidence:** standard (WCAG level A) — recorded as `author`. +- **Overlap:** states the same rule as `craft.wcag-contrast-and-dont-rely-on-colour-alone` and `modern.contrast-and-not-colour-alone`; kept because the colour pool needs it as its base and the measure (greyscale diff of states) is specific. + +### status-colours-keep-their-meaning +- **Claim:** "Elements using critical must convey messaging that implies that an action is impossible, blocked, or has resulted in an error." +- **Source:** Polaris, Palettes and roles, https://polaris.shopify.com/design/colors/palettes-and-roles — raw `palettes-and-roles.mdx`, `grep -F` exact. +- **Prefers / forbids (Polaris do/don't, grep-verified):** Success don't: "Use success to entice merchants or to share special offers." Warning don't: "Use warning for “under construction” or “coming soon” messaging." Info don't: "Use info to share statuses or messages that require immediate attention from the merchant." Critical don't: "Use critical for less important, non-actionable or contradicting messaging." +- **Corroboration:** GOV.UK colour: "Only use the variables in the context they're designed for." with the example of using `govuk-colour("red")` rather than `govuk-functional-colour("error")` for a plain red. Carbon data-vis alert palette: "Alert colors are used to reflect status." USWDS state tokens divide state colour into role families `info`, `error`, `warning`, `success`, `emergency`, `disabled`. +- **Measurable?** Partly → recorded as `count`: find the error/success colour from role=alert / invalid-field messages, count other painted elements in that colour that are not error/success/destructive. What counts as "destructive" needs a person on edge cases. +- **Evidence:** `author` (design-system guidance, no study cited). + +### from-tokens-not-hex +- **Claim:** "Do not copy the specific hexadecimal (hex) colour values." +- **Source:** GOV.UK Design System, Colour, https://design-system.service.gov.uk/styles/colour/ — raw `src/styles/colour/index.md`, `grep -F` exact. The next sentence in the source gives the example with code formatting: use `govuk-functional-colour("brand")` rather than `#1d70b8`. +- **Corroboration:** Carbon overview, Tokens: "They are used in place of hard coded values, like hex codes." (wrapped across lines; verified on collapsed text). USWDS overview: "**Use USWDS color tokens, and avoid custom colors whenever possible.**" (bold in source). Polaris ships a stylelint rule `color-no-hex` (file `tools/stylelint-polaris/rules/color-color-no-hex.mdx` fetched). +- **Measurable?** Yes: compare painted computed colours against values of custom properties on `:root`/theme selectors; count strays and near-duplicates (ΔE < 2). Limitation: a project that uses Sass variables compiled to literals has no custom properties to compare against; then the near-duplicate count is the only signal. +- **Evidence:** `author`. + +### few-families-in-proportion +- **Claim:** "about 60% of your site’s color would be the primary color family, about 30% would be the secondary color family, and about 10% would be the accent color families" (curly apostrophe as in source). +- **Source:** USWDS Theme color tokens, https://designsystem.digital.gov/design-tokens/color/theme-tokens/ — raw `theme-tokens.md`, `grep -F` exact. The sentence begins with bold role names (`**Primary**, **secondary**, and **accent** colors can be thought of as falling into a proportional 60/30/10 relationship:`); the quote is the unformatted clause after the colon. The source adds: "Note that these proportions are for non-base colors. In many cases, the neutral base text color will be the predominant tone on your site." +- **Corroboration:** Carbon overview: "The core blue family serves as the primary action color across all IBM products and experiences. Additional colors are used sparingly and purposefully." USWDS overview: "**Start in black and white.** … Then, introduce color to support that message." +- **Measurable?** Partly → `question`: hue-bin a screenshot's saturated pixels by area. The 60/30/10 split is a heuristic, not a threshold anyone validated, so it is not a count. +- **Evidence:** `author`; the 60/30/10 rule is interior-design folklore that USWDS adopts, with no study cited. +- **Overlap:** `craft.black-and-white-first-limit-hues`. + +### one-action-colour-apart-from-status +- **Claim:** "The core blue family serves as the primary action color across all IBM products and experiences. Additional colors are used sparingly and purposefully." +- **Source:** Carbon, Color overview, https://carbondesignsystem.com/elements/color/overview/ — raw `overview.mdx`, verified on collapsed text (wraps across two lines). +- **Corroboration:** Polaris Using color: "Link color is used exclusively for text links that appear in lines and paragraphs of text." Polaris Palettes and roles: "Brand is used to pull additional focus on main actions in the UI." and Success don't (above). +- **Counter-evidence kept as an exception:** Primer's colour-roles table maps `success` to "Primary buttons, positive messaging and successful states" and `accent` to "Links, selected, active, and focus states, and neutral information". GitHub's primary button shares the success green, so the rule "action colour ≠ status colour" is not universal; what the sources share is that the mapping is fixed and stated. +- **Measurable?** Yes: hue of body links and primary buttons vs the error/warning/success colours; count of static text in link colour. +- **Evidence:** `author`. +- **Overlap:** `canon.gestalt-similarity` (one link colour only for links). + +### controls-and-graphics-3-to-1 +- **Claim:** "Unless the control is inactive, any visual information provided that is necessary for a user to identify that a control is present and how to operate it must have a minimum 3:1 contrast ratio with the adjacent colors." +- **Source:** W3C, Understanding 1.4.11, https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html — raw `understanding/21/non-text-contrast.html`; the sentence wraps across lines in the HTML, verified on collapsed text. +- **Corroboration:** Primer color considerations: "Borders communicate **the boundaries of an input**, and are considered non-text contrast." and "Inputs without visible borders must use their background color instead." Primer accessibility lists "3:1 for UI elements and graphics" and "No contrast requirement for decorative and disabled elements". Carbon accessibility: "Visual information used to indicate states and boundaries of UI components must have a contrast ratio of 3:1 against adjacent colors." Polaris Using color, Icon: "These colors are tailored to meet color contrast ratios for interactive elements that do not include text." +- **Exceptions (WCAG):** inactive controls; a control with visible text needs no contrasting boundary ("Having a visual boundary indicating the hit area is only required when there is no other visual way to identify the presence of the control"); unrounded threshold ("2.999:1 would not meet the 3:1 threshold"); thin lines may anti-alias below nominal. +- **Measurable?** Yes: per-control contrast of border/fill/icon vs the background it sits on. No uxcli probe counts this today (`page.contrast` delegates to axe `color-contrast`, which is text only), so no `probe` is set. +- **Evidence:** standard (WCAG AA) — `author`. + +### focus-ring-contrasts-with-its-surroundings +- **Claim:** "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." +- **Source:** W3C, Understanding 1.4.11, section "Relationship with Focus Visible" — raw `understanding/21/non-text-contrast.html` line 119; the source has `<a>` around "2.4.7 Focus Visible" and `<em>` around "must", verified on tag-stripped text. +- **Detail (source):** the outer yellow `#FFFF00` ring on white fails; an outer green `#008000` passes but "it is not a good indicator unless it is very thick"; a focus border that changes hue inside the component must contrast with the component; "this success criterion does not directly compare the focused and unfocused states of a control". +- **Measurable?** Yes, as an extension of `page.focus-visible`'s pixel diff: take the changed pixels and their adjacent unchanged pixels and require 3:1. Not set as `probe` because `page.focus-visible` counts presence only. +- **Evidence:** standard — `author`. +- **Overlap:** related to but not the same as `modern.focus-is-visible-and-unobscured` (visibility and obscuring, not contrast), so not listed in `agrees`. + +### mode-aware-tokens-in-every-theme +- **Claim:** "Base color tokens don't respect color modes and should never be used directly in code or design." +- **Source:** Primer UI color system, https://primer.style/foundations/color/overview — raw `content/foundations/color/overview.mdx`, `grep -F` exact. Same file: "Every pattern in Primer is built to work across all color modes out of the box." +- **Corroboration:** Primer color considerations, Default theme: the default Day and Night themes "**must both** satisfy WCAG criteria for color-related concerns" (bold in source; quoted only here, not in the JSON). Non-default themes: deliberately low-contrast sub-themes such as Dark Dimmed need not meet contrast provided the defaults do (WCAG technique G174). Forced colours: "Forced color mode content presentation **should not** be overridden to satisfy aesthetic desires." Carbon overview: "In the dark themes, layers become one step lighter with each added layer." and the do/don't caption (JSX attribute, raw grep) "Do not apply components that are darker than the background unless using high-contrast mode." Carbon tokens make "possible color functionalities like inline theming and light or dark mode". +- **Measurable?** Yes: emulate `prefers-color-scheme` light and dark, run text and non-text contrast in each, count mode-only failures and elements whose background flipped while text did not. +- **Evidence:** `author`. + +### chart-palette-fits-the-data +- **Claim:** "Categorical (or qualitative) palettes are best when you want to distinguish discrete categories of data that do not have an inherent correlation." +- **Source:** Carbon, Data visualization color palettes, https://carbondesignsystem.com/data-visualization/color-palettes/ — raw `index.mdx`, verified on collapsed text. +- **Prefers / forbids (grep-verified):** Carbon: "In light themes, the darkest color denotes the largest values. In dark themes, the lightest color denotes the largest values." and "Never use a gradient in place of a sequential palette." Polaris data visualizations: "All bars should be the same color." USWDS data visualizations: "Simplify color selection and don't reuse colors for different variables or within a particular variable." Primer: "Sometimes there’s also meaning that is being conveyed with color that represents a state (error, success, critical), so in those cases, it can make sense to change the colors based on the semantic intent." +- **Measurable?** Partly → `question`: whether data are categorical or ordered is a reading of the data, not of the pixels. +- **Evidence:** `author`. +- **Overlap:** loosely `canon.tufte-graphical-integrity` (one visual dimension per data dimension). + +### chart-marks-readable-without-hue +- **Claim:** "Chart / visualization marks need to be at a 3:1 ratio with the background." +- **Source:** Primer, Data visualization, https://primer.style/ui-patterns/data-visualization — raw `content/ui-patterns/data-visualization.mdx`, `grep -F` exact. Same section: "Stacked bar charts and progress bars should have a high contrast divider line between each segment." and the note that "blue and purple look visually similar to some colorblind users". +- **Corroboration:** Primer accessibility: "For charts and graphs you can position the labels on top or close to each section. You can also use patterns to distinguish" (sentence continues on the next line: "different parts."). USWDS data visualizations: "If high contrast color selection is not an option, the usage of discrete dash or datapoint styles distinguishes lines without relying upon color." Polaris: "Use colors that can be distinguished from each other to support merchants with different forms of" (link follows). WCAG 1.4.11 Graphical Objects: "each line in a graph" is a graphical object. +- **Measurable?** Yes: mark-vs-plot-background contrast; presence of direct labels / dash / marker variation in multi-series charts. Canvas charts can only be read from pixels. +- **Evidence:** `author`. + +## Dropped + +None for lack of a verifiable quote. Considered and not made into viewpoints: USWDS "magic number" grade differences (a method for picking accessible pairs inside USWDS, not a rule about a rendered page; text contrast is already `page.contrast`); USWDS readability notes on avoiding pure black text (already `craft.greys-dont-have-to-be-grey-never-use-black`); Polaris "Use opacity or any other means to communicate disabled states." (a don't — Polaris-specific disabled scheme, too narrow); Polaris on-fill text pairing (system-internal). diff --git a/docs/lenses/data-display.md b/docs/lenses/data-display.md new file mode 100644 index 0000000..f800bcd --- /dev/null +++ b/docs/lenses/data-display.md @@ -0,0 +1,152 @@ +# Lens research — data display + +Researched 2026-10-03 for the uxcli "lenses" idea. This cluster covers how data is laid out and shown: tables, summary lists, numbers, empty and loading states for data, and charts. Sources are the published design systems of IBM (Carbon), GOV.UK (GDS), the U.S. government (USWDS), GitHub (Primer) and Shopify (Polaris), plus the W3C WAI tables tutorial. + +## How the sources were read + +Direct fetches of the public design-system sites were blocked from this session (every public URL below returned no response through the proxy). Each site's documentation lives in a public GitHub repository, so the source text was read from the raw Markdown/MDX that builds the public page, via `https://raw.githubusercontent.com/...`. File paths were found by a blobless `git clone --no-checkout` and `git ls-tree`. + +Every quote in `skills/uxcli/lenses/viewpoints/data-display.json` was checked by script against the fetched raw file after normalising only what the site renderer removes: Markdown emphasis (`**`), inline-code backticks, link syntax (`[text](url)` → `text`) and line wrapping/whitespace. Quotes that join two passages use ` … `; each part was checked separately. No quote was paraphrased. + +The `url` in each viewpoint is the public page that the raw file builds, derived from the repository's path and the site's URL scheme; it was **not** fetched directly. The raw file is named below for each. + +## Sources fetched + +| source | owner | public page (`url`) | raw file read | +|---|---|---|---| +| Data table — usage | IBM Carbon | https://carbondesignsystem.com/components/data-table/usage/ | carbon-design-system/carbon-website `main` `src/pages/components/data-table/usage.mdx` | +| Empty states pattern | IBM Carbon | https://carbondesignsystem.com/patterns/empty-states-pattern/ | carbon-design-system/carbon-website `main` `src/pages/patterns/empty-states-pattern/index.mdx` | +| Loading pattern | IBM Carbon | https://carbondesignsystem.com/patterns/loading-pattern/ | carbon-design-system/carbon-website `main` `src/pages/patterns/loading-pattern/index.mdx` | +| Axes and labels | IBM Carbon | https://carbondesignsystem.com/data-visualization/axes-and-labels/ | carbon-design-system/carbon-website `main` `src/pages/data-visualization/axes-and-labels/index.mdx` | +| Table | GOV.UK Design System | https://design-system.service.gov.uk/components/table/ | alphagov/govuk-design-system `main` `src/components/table/index.md` | +| Summary list | GOV.UK Design System | https://design-system.service.gov.uk/components/summary-list/ | alphagov/govuk-design-system `main` `src/components/summary-list/index.md` | +| Table (usability, accessibility, when to consider something else) | USWDS | https://designsystem.digital.gov/components/table/ | uswds/uswds-site `main` `_components/table/guidance/usability.md`, `accessibility.md`, `when-to-consider-something-else.md` | +| Data table | GitHub Primer | https://primer.style/product/components/data-table/ | primer/design `main` `content/components/data-table.mdx` | +| Loading | GitHub Primer | https://primer.style/product/ui-patterns/loading/ | primer/design `main` `content/ui-patterns/loading.mdx` | +| Empty states | GitHub Primer | https://primer.style/product/ui-patterns/empty-states/ | primer/design `main` `content/ui-patterns/empty-states.mdx` | +| Data visualization | GitHub Primer | https://primer.style/product/ui-patterns/data-visualization/ | primer/design `main` `content/ui-patterns/data-visualization.mdx` | +| Data table | Shopify Polaris | https://polaris-react.shopify.com/components/tables/data-table | Shopify/polaris `main` `polaris.shopify.com/content/components/tables/data-table.mdx` | +| Empty state | Shopify Polaris | https://polaris-react.shopify.com/components/layout-and-structure/empty-state | Shopify/polaris `main` `polaris.shopify.com/content/components/layout-and-structure/empty-state.mdx` | +| Data visualizations | Shopify Polaris | https://polaris-react.shopify.com/design/data-visualizations | Shopify/polaris `main` `polaris.shopify.com/content/design/data-visualizations.mdx` | +| Tables Tutorial; Tips and Tricks | W3C WAI (eds. Eric Eggert, Shadi Abou-Zahra) | https://www.w3.org/WAI/tutorials/tables/ , https://www.w3.org/WAI/tutorials/tables/tips/ | w3c/wai-tutorials `master-2.0` `content/tables/index.md`, `content/tables/tips.md` (front matter `permalink` gives the public path) | + +Note: the Polaris repository's data-visualizations page opens with "This section is currently being reworked to provide better guidance aligned with Polaris v12." Its guidance is still Shopify's published word, but it may change. + +Evidence grade: every entry is `author` — a design system's published guidance, not a study. Several of these systems cite research (Carbon's loading pattern cites NN/g on progress indicators), but the rules quoted here are their own. + +## Viewpoints + +### data-display.right-align-numbers-tabular-figures + +- **Source:** GitHub Primer, Data table, "Make numbers easier to compare". https://primer.style/product/components/data-table/ (raw: `primer/design/content/components/data-table.mdx`) +- **Quote:** "Right-align numeric values and use the tabular-num font variant when possible." (In the raw file `tabular-num` is inline code wrapped in a link to MDN `font-variant-numeric`; the link text and backticks were removed.) +- **Corroboration, verified the same way:** + - GOV.UK Table, "Numbers in a table": "When comparing columns of numbers, align the numbers to the right in table cells." + - USWDS Table usability: "Right-align numerical data." and "Use a monospace font for numerical data." The same bullet adds: "(There’s no need to apply monospace formatting or alignment to phone numbers, zip codes, dates, or other number content that can’t be totaled.)" This is the source of the identifier exception. + - Polaris Data table, Alignment: "Numerical = Right aligned". +- **Overlap:** `craft.align-with-readability-in-mind` and `modern.numbers-and-text-do-not-shift-layout` both touch tabular figures; this entry makes the table-cell rule specific and countable. + +### data-display.text-left-headers-follow-their-column + +- **Source:** W3C WAI Tables Tutorial, Tips and Tricks, "Alignment". https://www.w3.org/WAI/tutorials/tables/tips/ (raw: `w3c/wai-tutorials/content/tables/tips.md`, last_updated 2024-05-13) +- **Quote:** "Align text to the left and numeric data to the right (in left-to-right languages), so that people using larger text sizes or smaller screens will be able to find it. … It’s helpful to give column headers the same alignment as the data in the cells below." (The raw file has a double space in "It’s helpful", which HTML rendering collapses.) +- **Corroboration:** Polaris Data table, Alignment: "Textual data = Left aligned", "Align headers with their related data", "Don’t center align". + +### data-display.one-unit-and-format-per-column + +- **Source:** USWDS Table, usability guidance. https://designsystem.digital.gov/components/table/ (raw: `uswds/uswds-site/_components/table/guidance/usability.md`) +- **Quote:** "Predictably format columns. Take care not to vary units or formatting within the same column. Instead, normalize values so they can be easily compared." (The first sentence is bold in the raw file.) +- **Corroboration:** Polaris Data table: "Keep decimals consistent. For example, don’t use 3 decimals in one row and 2 in others." and headers should "Include units of measurement symbols so they aren’t repeated throughout the columns". Polaris's Do/Don't example is "Temperature °C" versus "Temperature". + +### data-display.header-cells-are-th-with-scope + +- **Source:** W3C WAI Tables Tutorial (index). https://www.w3.org/WAI/tutorials/tables/ (raw: `w3c/wai-tutorials/content/tables/index.md`, WCAG SC 1.3.1) +- **Quote:** "Header cells must be marked up with <th>, and data cells with <td> to make tables accessible. For more complex tables, explicit associations may be needed using scope, id, and headers attributes." (`<th>`, `<td>`, `scope`, `id` and `headers` are inline code in the raw file.) +- **Corroboration:** + - GOV.UK Table: "Use table headers to tell users what the rows and columns represent. Use the scope attribute to help users of assistive technology distinguish between row and column headers." + - USWDS Table accessibility: "Simple tables can have up to two rows of headers. Each header cell should have scope="col" or scope="row"." + - WAI Tips: "Table separation: If several tables follow one another, don’t use a single table and put in an additional row of <th> cells." + +### data-display.no-tables-for-layout + +- **Source:** GOV.UK Design System, Table, "When not to use this component". https://design-system.service.gov.uk/components/table/ (raw: `alphagov/govuk-design-system/src/components/table/index.md`) +- **Quote:** "Never use the table component to layout content on a page." +- **Corroboration:** + - USWDS, When to consider something else: "Don’t use tables in place of a layout grid." + - Polaris Data table accessibility, under "Don’t": "Use tables for layout." + - Primer Data table, "Use a list or something else": "When rows and columns are only a means of layout". +- The HTML-email exception is ours, not a quoted source, and is marked as such only by being in `exceptions`. + +### data-display.summary-list-for-key-value-facts + +- **Source:** GOV.UK Design System, Summary list. https://design-system.service.gov.uk/components/summary-list/ (raw: `alphagov/govuk-design-system/src/components/summary-list/index.md`) +- **Quote:** "Use a summary list to show information as a list of key facts. … only use it to present information that has a key and at least one value." The second passage is from "The summary list uses the description list (`<dl>`) HTML element, so only use it to present information that has a key and at least one value." +- **Also on the page:** "Do not use it for tabular data or a simple list of information or tasks". The page also describes visually hidden text on row actions, such as 'Change name', which is the source of the row-action preference. +- **Measure:** `question`. Whether a block of text is "a record's fields" is a judgement call, so no fixture-backed count was claimed. + +### data-display.long-tables-sort-and-say-so + +- **Source:** USWDS Table, usability guidance. https://designsystem.digital.gov/components/table/ (raw: `usability.md`) +- **Quote:** "Enable sort where useful. Add row sorting to individual columns of long tables where the data can be logically ordered either alphabetically or numerically." +- **Corroboration:** + - Primer Data table, Sorting: "If a table is sortable, it must start with one column sorted on page load." + - Polaris Data table: "Sortable tables use the aria-sort attribute to convey which columns are sortable (and in what direction)." + - USWDS accessibility: "Add an aria-live region to the page when enabling row sorting." + - USWDS usability, the merged-cell prohibition: "Don’t use row sorting with merged cells." + - Primer pagination: "20 rows is a good place to start." This is the threshold used in the measure. + +### data-display.empty-data-explains-and-offers-next-step + +- **Source:** IBM Carbon, Empty states pattern, Anatomy. https://carbondesignsystem.com/patterns/empty-states-pattern/ (raw: `carbon-website/src/pages/patterns/empty-states-pattern/index.mdx`) +- **Quote:** "Body: Explain clearly the next action to populate the space. You may also explain why the space is empty and include the benefit of taking this step." ("Body:" is bold in the raw file.) +- **Also from Carbon:** + - "Empty states always appear in the otherwise empty space, in the context of the data that’s missing." + - The page's table of no-data, user-action (no results) and error-management empty states. + - The multiple-empty-states rule: "we recommend using a tertiary button for the call to action." +- **Corroboration:** + - Primer Data table: "Communicate when the table has no data to show" / "Show a Blankslate component in place of the table". + - Primer Empty states, on errors with no fix: "secondary text can provide additional revelevant context or explain what they can do to get help". "revelevant" is sic in the source. + - Polaris Empty state: "Use only one primary call-to-action button". +- **Overlap:** `craft.dont-overlook-empty-states-teach-by-example` and `modern.every-state-is-designed` cover empty states in general. This entry adds placement in the data region and the distinction between first use, no results and error. + +### data-display.loading-skeleton-holds-the-layout + +- **Source:** GitHub Primer, Data table, "Use “skeleton” placeholders to indicate loading content". https://primer.style/product/components/data-table/ (raw: `data-table.mdx`) +- **Quote:** "Use “skeleton” placeholders to indicate loading content … The placeholder should match the real content as closely as possible. … Ideally, the height of the cell will not change once the data is loaded." The first passage is the section heading. +- **Corroboration:** + - Carbon Data table: "If extra load time is expected to display information, use skeleton states instead of spinners." + - Carbon Loading pattern: "Never represent toast notifications, overflow menus, dropdown items, modals, and loaders with skeleton states." + - Primer Loading: "Avoid creating a jarring layout shift when the loaded content replaces the loading indicator." and "Less than 1 second: Don't show a loading state." The second is the source of the sub-second exception. + - The Primer Loading caption: "Have a single loading announcement for a collection of skeleton loaders". + +### data-display.wrap-before-truncating-and-reveal-the-rest + +- **Source:** GitHub Primer, Data table, "Avoid wrapping or truncation of cell content as much as possible". https://primer.style/product/components/data-table/ +- **Quote:** "Cells that are likely to contain long strings may choose to wrap the content. This is the preferred way to accommodate long content because it doesn't hide the content. … Content truncation is available as a last resort when column widths are set. The full content may be exposed in a tooltip." +- **Corroboration:** + - Polaris Data table: "Wrap instead of truncate content. This is because if row titles start with the same word, they’ll all appear the same when truncated." + - Carbon Data table, column titles: "In cases where a column title is too long, wrap the text to two lines and then truncate the rest of the text. The full text should be shown in a tooltip on hover." +- "Truncating numbers" is listed in `forbids` as our inference from the same principle. No source quoted says it in those words. + +### data-display.chart-axes-are-honest + +- **Source:** IBM Carbon, Data visualization, Axes and labels. https://carbondesignsystem.com/data-visualization/axes-and-labels/ (raw: `carbon-website/src/pages/data-visualization/axes-and-labels/index.mdx`) +- **Quote:** "Always start numerical axes at zero for part-to-whole and comparisons charts, such as bar and area chart. … Never interpolate between periods when data is unavailable." +- **Exception from the same page:** "Line charts and scatter plots are less sensitive to this distortion because they are intended to communicate trends and not the relative size of the difference." The same page says "Never change axis ticks increments to accommodate data availability." +- **Overlap:** `canon.tufte-graphical-integrity` already says bars start at zero. This entry adds Carbon's line-chart exception and the rule against interpolating gaps. + +### data-display.chart-axes-and-bars-are-labelled + +- **Source:** Shopify Polaris, Data visualizations. https://polaris-react.shopify.com/design/data-visualizations (raw: `Shopify/polaris/polaris.shopify.com/content/design/data-visualizations.mdx`) +- **Quote:** "All standard charts that show quantitative data have 2 axes that should be labeled for clarity. … Label each bar with what it’s displaying, as well as the value." +- **Also from Polaris:** + - "Labelling should be outside and separate from the data area." + - Do "Skip labels in regular intervals." / Don't "Slant labels to make them fit." + - Horizontal bar charts, Don't use: "When the number of data points can exceed 6. In this case, use a table." +- **Corroboration:** Primer Data visualization anatomy table, Legend row: "Only show a legend when showing more than 1 data set". + +## Considered and dropped + +- **Row density and the hover state.** Carbon's Data table says "The data table’s row hover state should always be enabled as it can help the user visually scan the columns of data in a row even if the row is not interactive." It also offers five row sizes. Primer offers Condensed/Normal/Spacious density. Both quotes were verified, but the entry was dropped: the pool was already at twelve, and the guidance is a menu of options rather than a rule a rendered page can break. A future entry could count tables whose `tr:hover` changes nothing. +- **Empty cells shown as "-" or "x".** Primer: "You may show a message to explain an absence of data. Just don't use a character like “x” or “-”." This was verified. It was left out to keep the pool near the target size and because it is a single-source rule. It is a good candidate for the next revision. +- **GOV.UK "less data in tables".** "If possible, you should aim to have less data in your tables." This was verified but is too general to measure. diff --git a/docs/lenses/forms.md b/docs/lenses/forms.md new file mode 100644 index 0000000..469ca3c --- /dev/null +++ b/docs/lenses/forms.md @@ -0,0 +1,127 @@ +# Lens research — forms and input + +Researched 2026-10-03 for the uxcli lenses: the `forms` pool (`skills/uxcli/lenses/viewpoints/forms.json`), viewpoints about asking for, validating and recovering form input. + +## Method and verification + +The public design-system sites (design-system.service.gov.uk, designsystem.digital.gov, primer.style, w3.org) were not reachable from the research session; the proxy refused the connection. Every quote was therefore taken from the **source files those pages are built from**, fetched from GitHub raw this session with `curl`. Each quote was then checked by a script that strips Markdown backticks, the HTML tags of `.html` sources and ` ` entities, collapses whitespace, and looks for the quote as a substring of the fetched text. All 12 quotes passed. The `url` in each viewpoint is the public page built from that file, worked out from the repo's own permalinks and site layout. Nobody loaded it in a browser. + +Normalisation that touches a quote: +- `forms.input-type-matches-the-answer`: the Markdown wraps `<input type="number">` in backticks. The quote leaves them out, as the rendered page does. +- `forms.group-related-inputs-in-a-fieldset`: the USWDS HTML breaks the sentence over two lines. The quote joins them with a single space. + +Scope: these rules already exist in other pools and are not repeated here. Labels above fields (`usability.labels-above-fields`), placeholders are not labels (`usability.placeholders-are-not-labels`), single-column forms (`usability.single-column-forms`), field width (`usability.field-width-matches-expected-input`), one thing per page (`usability.one-thing-per-page`), blur validation (`usability.inline-validation-after-leaving-field`), checkout field count (`usability.fewer-checkout-fields`), tolerant input (`usability.postel-tolerant-input`), and the error-identification / error-prevention / redundant-entry flow probes. + +## Sources fetched + +| source | author | public URL | raw file fetched | +|---|---|---|---| +| Error summary component | GOV.UK Design System | https://design-system.service.gov.uk/components/error-summary/ | https://raw.githubusercontent.com/alphagov/govuk-design-system/main/src/components/error-summary/index.md | +| Error message component | GOV.UK Design System | https://design-system.service.gov.uk/components/error-message/ | …/src/components/error-message/index.md | +| Recover from validation errors | GOV.UK Design System | https://design-system.service.gov.uk/patterns/validation/ | …/src/patterns/validation/index.md | +| Question pages | GOV.UK Design System | https://design-system.service.gov.uk/patterns/question-pages/ | …/src/patterns/question-pages/index.md | +| Text input component | GOV.UK Design System | https://design-system.service.gov.uk/components/text-input/ | …/src/components/text-input/index.md | +| Date input component | GOV.UK Design System | https://design-system.service.gov.uk/components/date-input/ | …/src/components/date-input/index.md | +| Dates pattern | GOV.UK Design System | https://design-system.service.gov.uk/patterns/dates/ | …/src/patterns/dates/index.md | +| Fieldset component | GOV.UK Design System | https://design-system.service.gov.uk/components/fieldset/ | …/src/components/fieldset/index.md | +| Button component | GOV.UK Design System | https://design-system.service.gov.uk/components/button/ | …/src/components/button/index.md | +| Email addresses, Names, Addresses, Check answers patterns | GOV.UK Design System | https://design-system.service.gov.uk/patterns/ | …/src/patterns/{email-addresses,names,addresses,check-answers}/index.md | +| Understanding SC 1.3.5 Identify Input Purpose | W3C WAI | https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html | https://raw.githubusercontent.com/w3c/wcag/main/understanding/21/identify-input-purpose.html | +| Understanding SC 3.3.3 Error Suggestion | W3C WAI | https://www.w3.org/WAI/WCAG22/Understanding/error-suggestion.html | https://raw.githubusercontent.com/w3c/wcag/main/understanding/20/error-suggestion.html | +| Form component (accessibility guidance include) | USWDS | https://designsystem.digital.gov/components/form/ | https://raw.githubusercontent.com/uswds/uswds-site/main/_components/form/form.md and …/_includes/forms-guidance.html | +| Forms UI pattern | GitHub Primer | https://primer.style/product/ui-patterns/forms/overview/ | https://raw.githubusercontent.com/primer/design/main/content/ui-patterns/forms/overview.mdx | + +Tried and dropped: the W3C WAI forms tutorial (`w3c/wai-tutorials`, `content/forms/grouping.md` on `main` and `master`) returned 404, so the fieldset rule cites USWDS and GOV.UK instead. The GitHub tree API refused these repos in this session, so paths were found by trying the repos' own layouts. Shopify Polaris was not needed. + +## Viewpoints + +### forms.error-summary-at-the-top +- **Source:** GOV.UK Design System, Error summary component — https://design-system.service.gov.uk/components/error-summary/ +- **Quote:** "Always show an error summary when there is a validation error, even if there’s only one." +- **Supporting, same file:** "move keyboard focus to the error summary", "link to each of the answers that have validation errors", "Put the error summary at the top of the `main` container. If your page includes breadcrumbs or a back link, place it below these, but above the `<h1>`." +- **Second school:** Primer, Forms ("Validation on submit"): "If the form has 3 or more errors, you may show an [interactive summary of errors]", and "list the invalid inputs as anchor links. When the link is activated, place focus in its corresponding input." Primer's lower bound of 3 errors is recorded as an exception. +- **Verified:** substring match against raw `error-summary/index.md`. + +### forms.error-message-says-how-to-fix +- **Source:** GOV.UK Design System, Error message component, "Be clear and concise" — https://design-system.service.gov.uk/components/error-message/ +- **Quote:** "Describe what has happened and tell them how to fix it. The message must be in plain English, use positive language and get to the point." +- **Supporting, same file:** the "Do not use" list (jargon, 'please', 'sorry', 'valid'/'invalid', 'oops'); "Avoid messages like: … ‘This field is required’"; "Error messages should directly include language from the question or fieldset label." +- **Second school:** WCAG 3.3.3 Understanding, "What to do": "Where errors are detected, suggest known ways to correct them." +- **Verified:** substring match against raw `error-message/index.md` and `error-suggestion.html`. + +### forms.validate-when-the-user-is-done +- **Source:** GOV.UK Design System, Recover from validation errors — https://design-system.service.gov.uk/patterns/validation/ +- **Quote:** "Generally speaking, avoid validating the information in a field before the user has finished entering it. This sort of validation can cause problems - especially for users who type more slowly." +- **Supporting, same file:** "Do not validate when the user moves away from a field. Wait until they try to move to the next part of the service - usually by clicking the ‘continue’ or ‘submit’ button at the bottom of the page." and "Turn off HTML5 validation." +- **Second school:** Primer: "The default behavior of the web is to perform validation when the user attempts to submit the form. This lets the user flow quickly through the form without interruption." and "Don't attempt to validate an input before the user is done with it." +- **Disagreement, recorded rather than resolved:** GOV.UK says not to validate on blur. The existing `usability.inline-validation-after-leaving-field` (Wroblewski/Baymard) recommends blur validation for hard fields, and Primer allows it after a change plus blur. All three agree on *not while typing*, and that is what the measure checks. Because of the disagreement, the blur viewpoint is not listed in `agrees`. +- **Verified:** substring match against raw `validation/index.md` and `primer … overview.mdx`. + +### forms.keep-answers-after-an-error +- **Source:** GOV.UK Design System, Error message component, "How it works" — https://design-system.service.gov.uk/components/error-message/ +- **Quote:** "Do not clear any form fields when showing the Error message component. Keep both passing and failing answers." +- **Supporting:** Validation pattern: "show them the page again, with the form fields as the user filled them in". Check answers pattern: "If a user decides to go back to a previous answer, make sure information they've already entered is pre-populated." +- **Exception (password/CVV may be cleared):** a common security practice, not a GOV.UK statement. It is labelled as an exception, not a quote. +- **Verified:** substring match against raw `error-message/index.md`. + +### forms.mark-optional-fields-in-words +- **Source:** GOV.UK Design System, Question pages — https://design-system.service.gov.uk/patterns/question-pages/ +- **Quote:** "in most contexts, add ‘(optional)’ to the labels of optional fields" +- **Supporting, same file:** "Never mark mandatory fields with asterisks." +- **Disagreement:** USWDS form guidance says "Mark required fields as required by using a red asterisk (*)" with a key at the top of the form, *and* "Label optional fields with the word “optional” placed in parentheses." Primer: "When a field is required to have a value, it should be visibly marked as required." The schools agree that optional fields say so in words, and that is what the measure checks. The asterisk ban is GOV.UK's alone and is recorded in `exceptions`. +- **Verified:** substring match against raw `question-pages/index.md` and `forms-guidance.html`. + +### forms.autocomplete-names-the-purpose +- **Source:** W3C WAI, Understanding WCAG 2.2 SC 1.3.5 Identify Input Purpose — https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html +- **Quote:** "Use code to indicate the purpose of common inputs, where technology allows." (the "In brief → What to do" line) +- **Supporting, same file:** "This success criterion is specifically scoped to inputs collecting information about the user." This is the source of the exception for fields about other people. +- **Second school:** GOV.UK Text input: "Use the `autocomplete` attribute on text inputs to help users complete forms more quickly." The Names, Addresses, Email and Date input pages give the tokens (`name`, `postal-code`, `email`, `bday-day`/`bday-month`/`bday-year`). +- **Verified:** substring match against raw `understanding/21/identify-input-purpose.html` (tags stripped). The WCAG22 public URL serves the same Understanding document, which sits in the repo's `understanding/21/` folder because the SC was added in 2.1. + +### forms.input-type-matches-the-answer +- **Source:** GOV.UK Design System, Text input component, "Avoid using inputs with a type of number" — https://design-system.service.gov.uk/components/text-input/ +- **Quote:** "With <input type="number"> there’s a risk of users accidentally incrementing a number when they’re trying to do something else - for example, scroll up or down the page." (backticks around the element left out, as the page renders it) +- **Supporting, same file:** "If you're asking the user to enter a whole number, set the `inputmode` attribute to `numeric` …"; the guidance on negative numbers; "Do not disable copy and paste"; turning spellcheck off for names, references and emails. +- **Verified:** substring match against raw `text-input/index.md` after backtick removal. + +### forms.memorable-dates-as-three-fields +- **Source:** GOV.UK Design System, Date input component — https://design-system.service.gov.uk/components/date-input/ +- **Quote:** "Use the date input component when you’re asking users for a date they’ll already know, or can look up without using a calendar." +- **Supporting, same file:** "The date input component consists of 3 fields to let users enter a day, month and year."; "Never automatically tab users between the fields of the date input …"; "Accept month names written out in full or abbreviated form". Dates pattern: "Never make a calendar control that depends on JavaScript as the only input option."; the example-date rule (day ≥ 13, month ≤ 9); matching a document's format for document dates. +- **Inferred, not quoted:** "three dropdowns" in `forbids`. GOV.UK specifies text fields. The dropdown objection comes from `craft.anything-but-dropdowns`, which is listed in `agrees`. +- **Verified:** substring match against raw `date-input/index.md` and `dates/index.md`. + +### forms.ask-only-what-you-need +- **Source:** GOV.UK Design System, Question pages — https://design-system.service.gov.uk/patterns/question-pages/ +- **Quote:** "You should make sure you know why you’re asking every question and only ask users for information you really need." +- **Supporting, same file:** the link to a question protocol (Caroline Jarrett, UXmatters, 2010; the link was not fetched); "Make sure to only ask for a piece of information once within a single journey."; "allow users to answer ‘I do not know’ or ‘I’m not sure’ if they are valid responses". +- **Measure is a question:** whether a field is needed cannot be read off the page. +- **Verified:** substring match against raw `question-pages/index.md`. + +### forms.group-related-inputs-in-a-fieldset +- **Source:** USWDS, Form component, Accessibility guidance — https://designsystem.digital.gov/components/form/ +- **Quote:** "Group each set of thematically related controls in a fieldset element. Use the legend element to offer a label within each one." +- **Supporting, same file:** "Use a single legend for fieldset (this is required)." +- **Second school:** GOV.UK Fieldset: "The first element inside a fieldset must be a `legend` which describes the group of inputs." +- **Verified:** the public page's Markdown (`_components/form/form.md`) only includes `forms-guidance.html`, which holds the text. Substring match after tag stripping and whitespace collapse. + +### forms.hint-text-is-short-and-linked +- **Source:** GOV.UK Design System, Text input component, "Hint text" — https://design-system.service.gov.uk/components/text-input/ +- **Quote:** "Keep hint text to a single short sentence, without any full stops." +- **Supporting, same file:** "Do not include links within hint text. While screen readers will read out the link text when describing the field, they will not tell users that the text is a link." Question pages has the same rule and the statement-heading alternative for long explanations. Error message: "put the message in red after the question text and hint text", which fixes the order label → hint → error → input. +- **Second school:** Primer: "Caption text should be as short as possible." +- **Measure threshold:** "about 120 characters" is uxcli's way of making "short sentence" checkable. No source gives that number. +- **Verified:** substring match against raw `text-input/index.md`. + +### forms.do-not-disable-the-submit-button +- **Source:** GitHub Primer, Forms UI pattern, "Validation" — https://primer.style/product/ui-patterns/forms/overview/ +- **Quote:** "Disabled buttons are discouraged, as they don't clearly communicate what actions a user should take to complete a form." +- **Second school:** GOV.UK Button: "Disabled buttons have poor contrast and can confuse some users, so avoid them if possible." (` ` in source). USWDS: "Avoid disabled states, especially for text inputs." It allows them to prevent "multiple form submissions", recorded as an exception alongside GOV.UK's `data-prevent-double-click`. +- **URL caveat:** derived from the repo path `content/ui-patterns/forms/overview.mdx` and Primer's current `/product/` site section. Not loaded. +- **Verified:** substring match against raw `overview.mdx`, `button/index.md` and `forms-guidance.html`. + +## Considered and not added + +- **Check answers before submitting** (GOV.UK Check answers pattern): well sourced but out of the brief's list, and transaction-only. A candidate for a later pass. +- **Size inputs to the answer** and **one thing per page**: already covered by `usability.field-width-matches-expected-input` and `usability.one-thing-per-page`. GOV.UK Text input repeats the first ("Postcode inputs should be postcode-sized, phone number inputs should be phone number-sized."). +- **Do not restrict length with `maxlength`** (GOV.UK Text input): sourced and measurable. Left for a later pass to keep the pool near ten. diff --git a/docs/lenses/writing.md b/docs/lenses/writing.md new file mode 100644 index 0000000..4b5fa9c --- /dev/null +++ b/docs/lenses/writing.md @@ -0,0 +1,109 @@ +# Lens research — writing and microcopy + +Researched 2026-10-03 for the uxcli "lenses" idea. This cluster covers writing for interfaces: error messages, button and link labels, consistent terminology, capitalization, plain language, empty states, success and destructive-action messages, and the formatting of dates, numbers and units. The pool is `skills/uxcli/lenses/viewpoints/writing.json`. + +**How sources were verified.** The public design-system sites (design-system.service.gov.uk, carbondesignsystem.com, polaris.shopify.com, primer.style, styleguide.mailchimp.com) could not be fetched from this session: every request returned no response. All five publish their docs from open GitHub repositories, so each page's source file was downloaded from `https://raw.githubusercontent.com/<owner>/<repo>/HEAD/<path>` on 2026-10-03, with paths found through a shallow `git clone --no-checkout` and `git ls-tree`. Every quote below was then checked by a script: it collapses runs of whitespace to one space in both the quote and the file (the Markdown sources wrap lines), then tests for an exact substring match. All ten quotes matched. Curly quotes, apostrophes and the en dash in Carbon's "sentence–case" are as they appear in the source. The `url` in each viewpoint is the public page built from that file. The public URL is worked out from each repo's routing (Primer: `content/X.mdx` → `primer.style/X`; Carbon: `src/pages/X/index.mdx` → `carbondesignsystem.com/X/`; Polaris: `polaris.shopify.com/content/X.mdx` → `polaris.shopify.com/X`) and was **not** loaded in a browser. + +Overlap with existing pools is recorded in `agrees` rather than repeated: jargon and error codes (`usability.speak-the-users-language`), cutting words and sentence case in passing (`usability.omit-needless-words`), link text in navigation (`usability.mindless-clicks-not-fewer-clicks`), and the layout side of empty states (`craft.dont-overlook-empty-states-teach-by-example`, `modern.every-state-is-designed`). + +## Sources fetched + +| source | author | public URL | raw file verified against | +|---|---|---|---| +| Error message component | GOV.UK Design System (GDS) | https://design-system.service.gov.uk/components/error-message/ | `alphagov/govuk-design-system` `src/components/error-message/index.md` | +| Button component | GOV.UK Design System | https://design-system.service.gov.uk/components/button/ | `alphagov/govuk-design-system` `src/components/button/index.md` | +| Confirmation pages pattern | GOV.UK Design System | https://design-system.service.gov.uk/patterns/confirmation-pages/ | `alphagov/govuk-design-system` `src/patterns/confirmation-pages/index.md` | +| Dialog pattern | IBM Carbon | https://carbondesignsystem.com/patterns/dialog-pattern/ | `carbon-design-system/carbon-website` `src/pages/patterns/dialog-pattern/index.mdx` | +| Common actions pattern | IBM Carbon | https://carbondesignsystem.com/patterns/common-actions/ | `carbon-design-system/carbon-website` `src/pages/patterns/common-actions/index.mdx` | +| Content: Writing style | IBM Carbon | https://carbondesignsystem.com/guidelines/content/writing-style/ | `carbon-design-system/carbon-website` `src/pages/guidelines/content/writing-style.mdx` | +| Content: Action labels | IBM Carbon | https://carbondesignsystem.com/guidelines/content/action-labels/ | `carbon-design-system/carbon-website` `src/pages/guidelines/content/action-labels.mdx` | +| Empty states pattern | IBM Carbon | https://carbondesignsystem.com/patterns/empty-states-pattern/ | `carbon-design-system/carbon-website` `src/pages/patterns/empty-states-pattern/index.mdx` | +| Notifications pattern | IBM Carbon | https://carbondesignsystem.com/patterns/notification-pattern/ | `carbon-design-system/carbon-website` `src/pages/patterns/notification-pattern/index.mdx` | +| Content: Fundamentals | Shopify Polaris | https://polaris.shopify.com/content/fundamentals | `Shopify/polaris` `polaris.shopify.com/content/content/fundamentals.mdx` | +| Content: Error messages | Shopify Polaris | https://polaris.shopify.com/content/error-messages | `Shopify/polaris` `polaris.shopify.com/content/content/error-messages.mdx` | +| Content: Grammar and mechanics | Shopify Polaris | https://polaris.shopify.com/content/grammar-and-mechanics | `Shopify/polaris` `polaris.shopify.com/content/content/grammar-and-mechanics.mdx` | +| Toast component (internal only) | Shopify Polaris | https://polaris.shopify.com/components/internal-only/toast | `Shopify/polaris` `polaris.shopify.com/content/components/internal-only/toast.mdx` | +| Descriptive buttons | GitHub Primer | https://primer.style/guides/accessibility/descriptive-buttons | `primer/design` `content/guides/accessibility/descriptive-buttons.mdx` | +| Empty states | GitHub Primer | https://primer.style/ui-patterns/empty-states | `primer/design` `content/ui-patterns/empty-states.mdx` | +| Notification messaging | GitHub Primer | https://primer.style/ui-patterns/notification-messaging | `primer/design` `content/ui-patterns/notification-messaging.mdx` | +| Web Elements | Mailchimp Content Style Guide | https://styleguide.mailchimp.com/web-elements/ | `mailchimp/content-style-guide` `06-web-elements.html.md` | +| Writing Goals and Principles | Mailchimp Content Style Guide | https://styleguide.mailchimp.com/writing-principles/ | `mailchimp/content-style-guide` `01-writing-principles.html.md` | + +**Not used:** USWDS (`uswds/uswds-site`) was cloned but has no content-writing guide of its own; it points to plainlanguage.gov, which was not fetched. The GitHub REST API (`api.github.com/.../git/trees`) was refused for these repos from this session, so trees were listed through `git clone` instead. + +**Evidence strength, for all entries:** `author`. These are design-system house rules written by in-house content teams. GOV.UK's error-message page is the only one that cites research: it says the messages "have been tested with all types of users in live services, including tax credits". It reports outcomes (users "understood what went wrong", "knew how to fix the problem") but gives no numbers, so it is not rated `study`. + +## Viewpoints + +### errors-say-what-and-how-to-fix +- **Quote (verbatim):** "Describe what has happened and tell them how to fix it. The message must be in plain English, use positive language and get to the point." +- **Source:** GOV.UK Design System, Error message, section "Be clear and concise". https://design-system.service.gov.uk/components/error-message/ +- **Verified:** substring match in `govuk-design-system/src/components/error-message/index.md`. The same page gives the forbids list: "technical jargon like ‘form post error’, ‘unspecified error’ and ‘error 0x0000000643’"; "words like ‘forbidden’, ‘illegal’, ‘you forgot’ and ‘prohibited’"; "‘please’ because it implies a choice"; "‘sorry’ because it does not help fix the problem"; "‘valid’ and ‘invalid’ because they do not add anything to the message"; "humourous, informal language like ‘oops’". Under "Be specific" it lists generic messages to avoid ("An error occurred", "This field is required", …). Under "Match up error messages to labels" it says: "Error messages should directly include language from the question or fieldset label." +- **Corroboration:** Polaris Error messages: "Explain what’s wrong and what the merchant needs to do" and "Avoid error jargon like “invalid”". Primer empty states: "Vague messages like "There was a problem" can be frustrating." +- **Exceptions from source:** GOV.UK: "Do not use error messages to tell a user that they are not eligible or do not have permission to do something", and do not repeat an example that is already in the hint text. + +### buttons-name-the-action +- **Quote (verbatim):** "Use descriptive words for the actions like Add, Delete, Save and avoid vague words like Done or OK." +- **Source:** IBM Carbon, Dialog pattern, anatomy item 3 "Actions". https://carbondesignsystem.com/patterns/dialog-pattern/ +- **Verified:** substring match (whitespace-normalised; the source wraps after "Save") in `carbon-website/src/pages/patterns/dialog-pattern/index.mdx`. +- **Corroboration:** Mailchimp Web Elements: "Button copy should always include verbs." GOV.UK Button: "Write button text in sentence case, describing the action it performs." Primer Descriptive buttons: "A meaningful name describes the button’s purpose: the action that occurs when the button is activated". +- **Exception:** GOV.UK lists "‘Continue’ when the service does not save a user’s information" as correct for question pages. + +### links-describe-their-destination +- **Quote (verbatim):** "Don’t say things like “Click here!” or “Click for more information” or “Read this.” Write the sentence as you normally would, and link relevant keywords." +- **Source:** Mailchimp Content Style Guide, Web Elements → Links. https://styleguide.mailchimp.com/web-elements/ +- **Verified:** substring match in `mailchimp/content-style-guide/06-web-elements.html.md`. The curly quotes (U+201C/U+201D) and apostrophe (U+2019) were checked byte by byte with `od -c`. +- **Corroboration:** Polaris Grammar and mechanics → Links: "Never use “click here” or “here” for link text" and "If you have to include “learn more” links, avoid having more than one per screen". That second rule is the exception. + +### one-label-per-action +- **Quote (verbatim):** "When buttons perform the same action, they have the same name." +- **Source:** GitHub Primer, Descriptive buttons, "How to test names". https://primer.style/guides/accessibility/descriptive-buttons +- **Verified:** substring match in `primer/design/content/guides/accessibility/descriptive-buttons.mdx`. The next bullet gives the other half of the measure: "When buttons perform different actions, they don’t have the same name." The guidelines add: "Don’t reuse the same (visible) label or (invisible) name for buttons which perform different actions." +- **Corroboration:** Carbon Action labels: "Users rely on consistent labels for common actions to predict how to interact with an interface." Carbon Writing style: "TIP: Create a terminology list of words for your product that includes preferred words and words not to use." +- **Exception:** Carbon Common actions defines Remove separately from Delete ("as a removed item is not destroyed"), so the two can coexist when they mean different things. + +### sentence-case-ui-text +- **Quote (verbatim):** "Use sentence–case capitalization for all UI text elements." +- **Source:** IBM Carbon, Content → Writing style → Capitalization. https://carbondesignsystem.com/guidelines/content/writing-style/ +- **Verified:** substring match in `carbon-website/src/pages/guidelines/content/writing-style.mdx`. Note that the source writes "sentence–case" with an en dash (U+2013), kept as is. The same page says: "Title case can also slow reading and comprehension down" and "All caps capitalization has been shown to be slower to read". No study is linked for either, so evidence stays `author`. +- **Corroboration:** Polaris Grammar → Capitalization: "Use sentence case (capitalize the first word only) for:" headings, subheadings, buttons, card titles. GOV.UK Button: "Write button text in sentence case". +- **Dissent, recorded as an exception:** Mailchimp Web Elements uses "title case for menu names", for form titles, for "main or global navigation" and for page titles. House styles differ on titles and navigation. They agree on buttons and field labels. + +### plain-language-reading-level +- **Quote (verbatim):** "Aim for a 7th grade reading level—it’s easiest for merchants to digest" +- **Source:** Shopify Polaris, Content → Fundamentals, "Write like merchants talk". https://polaris.shopify.com/content/fundamentals +- **Verified:** substring match in `Shopify/polaris/polaris.shopify.com/content/content/fundamentals.mdx`. The em dash (U+2014) and apostrophe (U+2019) are in the source. The same section says "Use plain language" and "Some jargon is okay, as long as it’s what actual merchants say". "Inspire action" says "Start sentences with verbs so they feel like actionable instructions". +- **Corroboration:** Carbon Writing style: "Be succinct, and keep sentences as short and simple as possible." Mailchimp principles: "Clear. Understand the topic you’re writing about. Use simple words and sentences." +- **Measure note:** the grade-9 and 25-word thresholds in `measure.what` are uxcli's tolerance around Polaris's 7th-grade target. The source does not give them. + +### empty-states-say-what-next +- **Quote (verbatim):** "From the text, users should be able to understand what steps they might take next." +- **Source:** GitHub Primer, UI patterns → Empty states, "Secondary text". https://primer.style/ui-patterns/empty-states +- **Verified:** substring match in `primer/design/content/ui-patterns/empty-states.mdx`. The same page, under "Primary action", says: "If the space is empty because this the feature hasn't been used yet, the action should initiate a creation flow or link to a feature." Its error-state guidance gives the forbids ("There was a problem"; the over-literal "US East-2 database cluster" example). +- **Corroboration:** Carbon Empty states pattern, anatomy "Body": "Explain clearly the next action to populate the space." +- **Measure note:** the 6-word floor is uxcli's. The source does not give it. + +### success-names-what-happened +- **Quote (verbatim):** "Written in the pattern of: noun + verb" +- **Source:** Shopify Polaris, Toast component (internal only), Content guidelines → Message. https://polaris.shopify.com/components/internal-only/toast +- **Verified:** substring match in `Shopify/polaris/polaris.shopify.com/content/components/internal-only/toast.mdx`. The same text appears in the deprecated public Toast page, `content/components/deprecated/toast.mdx`. Its Do list: "Product updated", "Collection added". Its Don't list: "Your product has been successfully updated", "Discount: Saved successfully", "Your Order was Archived Today". The page is filed as internal-only/deprecated, but its content rule is unchanged and is the most specific source found. +- **Corroboration and exception:** Primer Notification messaging, "Success": "Use success messaging sparingly and rely more on interaction context". A message is not needed when, for example, "a user is brought to the newly created issue page upon successful creation". Its needed example is an inline message reading "Issue #21 created". Carbon Notifications gives "Success! Your resource has been created." as a Do example. That contradicts the noun-first pattern, so the pool follows Polaris and Primer. + +### destructive-actions-name-the-consequence +- **Quote (verbatim):** "Clearly describe the action being confirmed and explain any potential consequences that it may cause. Both the title and the button should reflect the action that will occur." +- **Source:** IBM Carbon, Dialog pattern → "Confirm a user decision". https://carbondesignsystem.com/patterns/dialog-pattern/ +- **Verified:** substring match (whitespace-normalised) in `carbon-website/src/pages/patterns/dialog-pattern/index.mdx`. +- **Corroboration:** Carbon Common actions → Delete: "Ask for confirmation of the delete, with guidance about what will occur if they delete". For high-impact deletion: "have the user type the name of the resource they are deleting". GOV.UK Button → Warning buttons: "Do not only rely on the red colour of a warning button to communicate the serious nature of the action" and "Make sure the context and button text make clear what will happen if the user selects it." +- **Exception from source:** Carbon "Low-impact deletion": "Delete the data upon click or tap without further warning." + +### dates-numbers-units-for-the-reader +- **Quote (verbatim):** "Use the month’s full name. If there isn’t enough space, use 3-letter abbreviations. Don’t write dates with numerals only." +- **Source:** Shopify Polaris, Content → Grammar and mechanics → "Numbers, dates, and currency". https://polaris.shopify.com/content/grammar-and-mechanics +- **Verified:** substring match in `Shopify/polaris/polaris.shopify.com/content/content/grammar-and-mechanics.mdx`. The same section also says "Use numerals", "Use commas for numbers with four or more digits. Avoid shortening numbers." (Don't: "12 k"), "Don’t use ordinal indicators", "Include a space between the number and the unit." (Don't: "3.4lb"), and "the currency comes after the dollar amount". +- **Exception from source:** "Dates, numbers, and measurements are often formatted automatically according to users’ local preferences … These guidelines are for manually formatting in American English". The viewpoint therefore asks for the reader's locale, not US order. + +## Dropped + +None of the planned viewpoints was dropped for an unverifiable quote. Two planned topics were folded into others instead of standing alone: +- *Avoid jargon and internal system names* is already `usability.speak-the-users-language`. It is folded into the forbids of `errors-say-what-and-how-to-fix` and `plain-language-reading-level`. +- *Specific, label-matched error messages* (GOV.UK "Be specific" / "Match up error messages to labels") is on the same source page as the main error rule. It is folded into `errors-say-what-and-how-to-fix`, whose measure checks it. diff --git a/examples/crm/.uxcli/index.json b/examples/crm/.uxcli/index.json index cef5da2..d850fe5 100644 --- a/examples/crm/.uxcli/index.json +++ b/examples/crm/.uxcli/index.json @@ -7,13 +7,13 @@ "story": "L2 — Verify", "toNext": { "trust → gate": [ - "probe lead-phone-visible: fpRate chưa đo (1 corpus case, cần ≥ N) → chưa được fail", - "C-002: cả hai measurement method-unproven → chưa gate được", - "C-003 RETIREMENT_PROPOSED từ 2026-09-24, chưa ai quyết" + "probe lead-phone-visible: fpRate not measured (1 corpus case, needs ≥ N) → may not fail yet", + "C-002: both measurements method-unproven → cannot gate yet", + "C-003 RETIREMENT_PROPOSED since 2026-09-24, nobody has decided" ], "reach → inject": [ - "recoverability của lead-new-unassigned unverified: cleanup chưa từng chạy sạch trên run fail", - "environment local chưa có run nào" + "recoverability of lead-new-unassigned unverified: cleanup has never run clean on a failed run", + "environment local has no run yet" ] } }, @@ -33,7 +33,7 @@ }, "I-003": { "confidence": "hypothesis", - "why": "không evidence" + "why": "no evidence" } }, "commitments": { diff --git a/examples/crm/.uxcli/proposals/P-0003.json b/examples/crm/.uxcli/proposals/P-0003.json index 73c4b03..176ade1 100644 --- a/examples/crm/.uxcli/proposals/P-0003.json +++ b/examples/crm/.uxcli/proposals/P-0003.json @@ -6,7 +6,7 @@ "ref": "claude", "onBehalfOf": "chuongld" }, - "statement": "Cam kết: hành động Gọi khách trong viewport không cuộn tại agent.lead_detail", + "statement": "Cam kết: hành động Gọi khách trong viewport không cuộn at agent.lead_detail", "trace": [ "understanding/insights/I-001.json", "journeys/handle-inbound-lead.json#open-and-call/s2" diff --git a/examples/crm/.uxcli/runs/R-2026-09-25T08-30-02Z-uzdc8z/run.json b/examples/crm/.uxcli/runs/R-2026-09-25T08-30-02Z-uzdc8z/run.json index 3e797d2..82dd4a6 100644 --- a/examples/crm/.uxcli/runs/R-2026-09-25T08-30-02Z-uzdc8z/run.json +++ b/examples/crm/.uxcli/runs/R-2026-09-25T08-30-02Z-uzdc8z/run.json @@ -24,10 +24,10 @@ "blocked": { "reason": "prerequisite_not_satisfied", "which": [ - "s1.before agent.workspace_ready: không identity — production reach observe, testIdentity không áp dụng", - "f0 lead-new-unassigned: fixture cần mutate, effective reach là observe" + "s1.before agent.workspace_ready: no identity — production reach observe, testIdentity does not apply", + "f0 lead-new-unassigned: fixture needs mutate, effective reach is observe" ], - "note": "không probe nào chạy; không có verdict; không tính vào FP rate hay level" + "note": "no probe ran; no verdict; not counted toward the FP rate or the level" }, "steps": [], "verdicts": [] diff --git a/examples/crm/.uxcli/runs/R-2026-09-25T08-41-12Z-zs8fns/run.json b/examples/crm/.uxcli/runs/R-2026-09-25T08-41-12Z-zs8fns/run.json index a58d931..b75665f 100644 --- a/examples/crm/.uxcli/runs/R-2026-09-25T08-41-12Z-zs8fns/run.json +++ b/examples/crm/.uxcli/runs/R-2026-09-25T08-41-12Z-zs8fns/run.json @@ -41,7 +41,7 @@ "tenant": true }, "cleanup": "retained", - "retainedBecause": "run có fail — fixture là bằng chứng", + "retainedBecause": "the run has a fail — the fixture is evidence", "expiresAt": "2026-09-27T08:41:12Z" } ] @@ -238,7 +238,7 @@ { "path": "s1.response.internalScore", "kept": false, - "why": "không step nào consume" + "why": "no step consumes it" } ], "ruleSnapshots": { @@ -279,7 +279,7 @@ "value": "fail", "caps": [], "cause": "probe-said", - "what": "[data-uxcli=call-action] cần 2 lần cuộn ở 390×844 tại agent.lead_detail", + "what": "[data-uxcli=call-action] cần 2 lần cuộn ở 390×844 at agent.lead_detail", "where": "s2", "shot": "s2-before.png" }, @@ -288,7 +288,7 @@ "measurement": 0, "value": "not-applicable", "caps": [], - "cause": "toStable 611ms < 1000ms — điều kiện không xảy ra" + "cause": "toStable 611ms < 1000ms — the condition did not occur" }, { "commitment": "C-002", diff --git a/examples/crm/.uxcli/versions/handle-inbound-lead/v1.json b/examples/crm/.uxcli/versions/handle-inbound-lead/v1.json new file mode 100644 index 0000000..39d3b05 --- /dev/null +++ b/examples/crm/.uxcli/versions/handle-inbound-lead/v1.json @@ -0,0 +1,9 @@ +{ + "schema_version": 1, + "journey": "handle-inbound-lead", + "name": "v1", + "run": "runs/R-2026-09-24T09-12-03Z-dujexe", + "at": "2026-09-24T09:30:00Z", + "by": { "type": "person", "ref": "example-owner" }, + "note": "First walk: open the lead from the list." +} diff --git a/examples/crm/.uxcli/versions/handle-inbound-lead/v2.json b/examples/crm/.uxcli/versions/handle-inbound-lead/v2.json new file mode 100644 index 0000000..86bf27f --- /dev/null +++ b/examples/crm/.uxcli/versions/handle-inbound-lead/v2.json @@ -0,0 +1,9 @@ +{ + "schema_version": 1, + "journey": "handle-inbound-lead", + "name": "v2", + "run": "runs/R-2026-09-25T08-41-12Z-zs8fns", + "at": "2026-09-25T09:00:00Z", + "by": { "type": "person", "ref": "example-owner" }, + "note": "The walk now seeds its own lead and goes on to the call." +} diff --git a/package.json b/package.json index f21a40b..d634a0c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@junixlabs/uxcli", "version": "0.8.0", - "description": "Give a coding agent the product context it needs before it designs a screen, then measure the screen in real Chrome before it says done. No commitment, no verdict.", + "description": "Make a coding agent design like a designer who did the research: a cited library of UI/UX rules and product templates, the context to read before designing, and journeys walked in real Chrome to check the experience before it says done.", "license": "MIT", "repository": { "type": "git", diff --git a/schemas/proposal.schema.json b/schemas/proposal.schema.json index 01ba81e..7498aab 100644 --- a/schemas/proposal.schema.json +++ b/schemas/proposal.schema.json @@ -24,7 +24,8 @@ "retirement", "profile", "journey", - "insight" + "insight", + "redesign" ] }, "proposedBy": { diff --git a/schemas/template.schema.json b/schemas/template.schema.json new file mode 100644 index 0000000..c70e8a7 --- /dev/null +++ b/schemas/template.schema.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/junixlabs/uxcli/schemas/template.schema.json", + "title": "uxcli project template", + "description": "A project template — skills/uxcli/templates/<id>.json: the screens a kind of product usually has and the lens each is read against, the journeys worth walking first and what to watch on them, and the questions research must answer. It states no fact about any product's users.", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "id", "name", "when", "actors", "screens", "journeys", "research", "sources"], + "properties": { + "schema_version": { "const": 1 }, + "id": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, + "name": { "type": "string", "minLength": 1 }, + "when": { "type": "string", "minLength": 1, "description": "what kind of product this template is for" }, + "actors": { + "type": "array", "minItems": 1, + "items": { + "type": "object", "additionalProperties": false, "required": ["actor", "who", "questions"], + "properties": { + "actor": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, + "who": { "type": "string", "minLength": 1 }, + "questions": { "type": "array", "minItems": 3, "items": { "type": "string", "pattern": "\\?\\s*$" }, "description": "written into the actor's unknowns[] by template apply" } + } + } + }, + "screens": { + "type": "array", "minItems": 1, + "items": { + "type": "object", "additionalProperties": false, "required": ["id", "name", "lens", "does"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, + "name": { "type": "string", "minLength": 1 }, + "lens": { "enum": ["marketing", "content", "data", "workspace", "shop", "transaction"] }, + "does": { "type": "string", "minLength": 1 } + } + } + }, + "journeys": { + "type": "array", "minItems": 1, + "items": { + "type": "object", "additionalProperties": false, "required": ["id", "goal", "actor", "through", "watch"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, + "goal": { "type": "string", "minLength": 1 }, + "actor": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, + "through": { "type": "array", "minItems": 2, "items": { "type": "string" } }, + "watch": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } } + } + } + }, + "research": { "type": "array", "minItems": 3, "items": { "type": "string", "pattern": "\\?\\s*$" } }, + "sources": { + "type": "array", "minItems": 1, + "items": { + "type": "object", "additionalProperties": false, "required": ["name", "url", "why"], + "properties": { "name": { "type": "string", "minLength": 1 }, "url": { "type": "string", "pattern": "^https?://" }, "why": { "type": "string", "minLength": 1 } } + } + } + } +} diff --git a/schemas/version.schema.json b/schemas/version.schema.json new file mode 100644 index 0000000..6e46cb1 --- /dev/null +++ b/schemas/version.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/junixlabs/uxcli/schemas/version.schema.json", + "title": "uxcli version", + "description": "A version of a journey — .uxcli/versions/<journey>/<name>.json: a name given to one walk of it, so it is kept against pruning and can be compared with another (uxcli experience --from --to). Created by uxcli version save, never edited.", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "journey", "name", "run", "at", "by"], + "properties": { + "schema_version": { "const": 1 }, + "journey": { "type": "string", "minLength": 1 }, + "name": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" }, + "run": { "type": "string", "pattern": "^runs/R-[^/]+$", "description": "the walk this version is" }, + "at": { "type": "string", "minLength": 1 }, + "by": { "type": "object", "additionalProperties": false, "required": ["type", "ref"], "properties": { "type": { "type": "string", "minLength": 1 }, "ref": { "type": "string", "minLength": 1 } } }, + "note": { "type": "string", "minLength": 1, "description": "what changed in this version, in words" }, + "build": { "type": "string", "minLength": 1, "description": "the commit the walk ran against" }, + "proposal": { "type": "string", "minLength": 1, "description": "the redesign proposal this version answers" } + } +} diff --git a/skills/uxcli/SKILL.md b/skills/uxcli/SKILL.md index e4c8a6d..d3e5577 100644 --- a/skills/uxcli/SKILL.md +++ b/skills/uxcli/SKILL.md @@ -32,6 +32,8 @@ Use this bounded path for ordinary UI work. Read a reference only where a step n needs, the commitments already signed over it, and what the last run saw. Read it top to bottom. If there is no journey or no understanding on disk, the card says so; go to `references/understand.md` and `references/journey.md` before writing any UI. + If you do not know the product's field from sources, research it first: `npx -y @junixlabs/uxcli + template` names a starting point for the kind of product, and `references/research.md` is how. 3. **Draw before you build.** First pick the lens for what the person does on this screen — `marketing`, `content`, `data`, `workspace`, `shop` or `transaction` — and read its checklist, `lenses/<kind>.md` in this skill: named designers' rules, each with its source and what to look @@ -60,6 +62,9 @@ Use this bounded path for ordinary UI work. Read a reference only where a step n Read each card. A `fail` names the element, the place, the rule and how to check it yourself; fix that element and run again. Never edit a journey, a commitment or a probe to make a run pass. Then review the built screen through its lens, the same way, and `uxcli review check`. + Then read what the person goes through: `npx -y @junixlabs/uxcli experience --page` — steps, clicks, + typing, scrolls, waits and a keystroke-level estimate per journey, each finding pinned on the step's + screenshot, and what moved since the walk before. Report each finding in its words; they never block. 6. **Say done only at exit 0**, and say what was measured. Anything else, say what stands in the way. `references/before-done.md` is the full sequence and the list of what exit 0 does not cover. @@ -96,6 +101,9 @@ Before you design or change a screen, you read `uxcli context show` for the jour | `references/journey.md` | the card says no journey, or a flow has changed shape: states as signals, hooks, workflows | | `references/mockups.md` | a screen the journey names has no mockup or no pick: variants, hooks, `pick.json` | | `references/understand.md` | the card says no understanding, or an insight the screen leans on is missing or a hypothesis | +| `references/versions.md` | a walk showed something worth changing: naming versions, a redesign proposal with evidence, comparing two versions | +| `references/research.md` | you do not know the product's field from sources yet: where to look, what to come back with, how to write it down | +| `templates/<id>.md` | the product is a kind uxcli has a template for — `workspace`, `shop`, `landing`: screens and their lens, journeys to walk, what to research | | `lenses/<kind>.md` | before you draw or change a screen: the checklist for its kind of UI — `marketing`, `content`, `data`, `workspace`, `shop`, `transaction` | | `references/lenses.md` | how to pick the lens, answer it, write the review, and pass `review check` | | `references/principles.md` | a verdict is `not-committed` and the project should decide; proposing a commitment | diff --git a/skills/uxcli/lenses/content.json b/skills/uxcli/lenses/content.json index 3254020..ac0ea8d 100644 --- a/skills/uxcli/lenses/content.json +++ b/skills/uxcli/lenses/content.json @@ -22,6 +22,10 @@ { "viewpoint": "usability.no-false-affordances" }, + { + "viewpoint": "usability.mindless-clicks-not-fewer-clicks", + "note": "Same rule, also stated as: writing.links-describe-their-destination (Mailchimp)." + }, { "viewpoint": "usability.follow-conventions", "note": "Same rule, also stated as: modern.boring-and-familiar-beats-novel (Scott Berkun)." @@ -35,9 +39,6 @@ { "viewpoint": "usability.natural-mapping-and-proximity" }, - { - "viewpoint": "usability.mindless-clicks-not-fewer-clicks" - }, { "viewpoint": "usability.start-with-user-needs-design-with-data" }, @@ -56,6 +57,10 @@ "viewpoint": "craft.body-16px-line-height-1-5", "note": "Same rule, also stated as: canon.butterick-body-size-15-25px (Matthew Butterick); modern.readable-type-sizes-and-weights (Rauno Freiberg)." }, + { + "viewpoint": "craft.wcag-contrast-and-dont-rely-on-colour-alone", + "note": "Same rule, also stated as: modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group)." + }, { "viewpoint": "craft.line-length-50-75-characters", "note": "Same rule, also stated as: canon.bringhurst-measure-45-75 (Robert Bringhurst)." @@ -64,10 +69,6 @@ "viewpoint": "craft.center-text-only-under-three-lines", "note": "Same rule, also stated as: canon.vignelli-flush-left (Massimo Vignelli)." }, - { - "viewpoint": "craft.wcag-contrast-and-dont-rely-on-colour-alone", - "note": "Same rule, also stated as: modern.contrast-and-not-colour-alone (Apple)." - }, { "viewpoint": "craft.size-isnt-everything-use-weight-and-colour" }, @@ -86,9 +87,6 @@ { "viewpoint": "canon.bringhurst-leading-is-a-rhythmic-unit" }, - { - "viewpoint": "modern.cards-are-a-choice-not-a-default" - }, { "viewpoint": "modern.no-junk-drawer-menus-or-unlabeled-icons", "note": "Same rule, also stated as: usability.dont-make-me-think (Steve Krug)." @@ -110,6 +108,31 @@ }, { "viewpoint": "modern.defaults-are-decisions-you-inherited" + }, + { + "viewpoint": "color.from-tokens-not-hex" + }, + { + "viewpoint": "color.one-action-colour-apart-from-status" + }, + { + "viewpoint": "color.focus-ring-contrasts-with-its-surroundings" + }, + { + "viewpoint": "color.few-families-in-proportion" + }, + { + "viewpoint": "writing.sentence-case-ui-text" + }, + { + "viewpoint": "writing.dates-numbers-units-for-the-reader" + }, + { + "viewpoint": "data-display.no-tables-for-layout", + "note": "Same rule, also stated as: modern.cards-are-a-choice-not-a-default (Stan Kirilov)." + }, + { + "viewpoint": "data-display.header-cells-are-th-with-scope" } ] } diff --git a/skills/uxcli/lenses/content.md b/skills/uxcli/lenses/content.md index 3b94e01..f93a15b 100644 --- a/skills/uxcli/lenses/content.md +++ b/skills/uxcli/lenses/content.md @@ -2,7 +2,7 @@ Docs, articles, blogs and help centres: long text read and navigated by section. -32 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. +39 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. ## 1. `usability.signifiers-make-clickable-look-clickable` @@ -61,7 +61,19 @@ Nothing that is not interactive may look interactive: no underlined or blue stat - **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons - **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). -## 6. `usability.follow-conventions` +## 6. `usability.mindless-clicks-not-fewer-clicks` + +Do not count clicks; make each one an unambiguous choice with clear link text that says where it goes. + +- **Source:** Steve Krug, *Don't Make Me Think, Revisited — chapter 4 (Krug's Second Law of Usability)* — https://ptgmedia.pearsoncmg.com/images/9780321965516/samplepages/0321965515.pdf +- **In their words:** "It doesn't matter how many times I have to click, as long as each click is a mindless, unambiguous choice." +- **Do:** Write link text that identifies its target; Use breadcrumbs and hub pages for scent; Keep clear labelling with strong information scent +- **Don't:** Count clicks as the metric; Use generic links like 'Click here' or 'Learn more' +- **Look at:** Count links and buttons whose text is generic ('Click here', 'Learn more', 'Read more', bare 'Next') or duplicates another link's text with a different target; do not measure path length. +- **Unless:** Fewer clicks matter more when the same path is drilled repeatedly or pages take long to load (Krug) +- **Also stated as:** writing.links-describe-their-destination (Mailchimp). + +## 7. `usability.follow-conventions` Work the way the sites and platforms users already know; a convention beats a locally optimised novelty. @@ -73,7 +85,7 @@ Work the way the sites and platforms users already know; a convention beats a lo - **Unless:** Desktop apps follow their own OS: Windows OK-first, Apple OK-last - **Also stated as:** modern.boring-and-familiar-beats-novel (Scott Berkun). -## 7. `usability.banner-blindness-dont-style-content-like-ads` +## 8. `usability.banner-blindness-dont-style-content-like-ads` Keep essential content and the primary action in the main column, styled like content, never in a right rail, top strip or animated coloured box. @@ -84,7 +96,7 @@ Keep essential content and the primary action in the main column, styled like co - **Look at:** Is the journey's primary action or a required notice positioned in the right rail (x > 70% of desktop viewport) or in a full-width top strip with background fill and animation? - **Unless:** On mobile, large inline ads do get fixated — the effect is weaker for inline placement -## 8. `usability.aesthetic-usability-effect-bias` +## 9. `usability.aesthetic-usability-effect-bias` Polish makes a screen look more usable than it is; discount your aesthetic impression and run the measurable checks first. @@ -95,7 +107,7 @@ Polish makes a screen look more usable than it is; discount your aesthetic impre - **Look at:** Did the evaluator judge the screen usable because it looks good? Re-check the verdict against the count-kind entries before trusting it; 'attractive' is the variable and stays taste. - **Unless:** With severe usability issues, or functionality sacrificed for aesthetics, users lose patience -## 9. `usability.natural-mapping-and-proximity` +## 10. `usability.natural-mapping-and-proximity` Put a control next to the thing it changes and a label next to its field; gaps between groups must exceed gaps within them. @@ -106,17 +118,6 @@ Put a control next to the thing it changes and a label next to its field; gaps b - **Look at:** Distance from each label to its own input versus the nearest other input; gap within a fieldset versus gap between fieldsets; distance from last input to the submit button. - **Unless:** Left-aligned labels are acceptable if space is constrained and labels are of similar length -## 10. `usability.mindless-clicks-not-fewer-clicks` - -Do not count clicks; make each one an unambiguous choice with clear link text that says where it goes. - -- **Source:** Steve Krug, *Don't Make Me Think, Revisited — chapter 4 (Krug's Second Law of Usability)* — https://ptgmedia.pearsoncmg.com/images/9780321965516/samplepages/0321965515.pdf -- **In their words:** "It doesn't matter how many times I have to click, as long as each click is a mindless, unambiguous choice." -- **Do:** Write link text that identifies its target; Use breadcrumbs and hub pages for scent; Keep clear labelling with strong information scent -- **Don't:** Count clicks as the metric; Use generic links like 'Click here' or 'Learn more' -- **Look at:** Count links and buttons whose text is generic ('Click here', 'Learn more', 'Read more', bare 'Next') or duplicates another link's text with a different target; do not measure path length. -- **Unless:** Fewer clicks matter more when the same path is drilled repeatedly or pages take long to load (Krug) - ## 11. `usability.start-with-user-needs-design-with-data` Name the actor and their need before drawing a screen, then let measured behaviour, not hunches or looks, decide what changes. @@ -173,7 +174,19 @@ Body copy at 16px or more with 1.5 line height; inputs at 16px or more on mobile - **Unless:** Interaction-heavy desktop pages may go to 14px; Captions sit 2px under body by design - **Also stated as:** canon.butterick-body-size-15-25px (Matthew Butterick); modern.readable-type-sizes-and-weights (Rauno Freiberg). -## 16. `craft.line-length-50-75-characters` +## 16. `craft.wcag-contrast-and-dont-rely-on-colour-alone` + +Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. + +- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html +- **In their words:** "The WCAG recommended contrast ratio for body text — 4.5:1 to meet AA standards" +- **Do:** Style coloured badges as a soft background with dark text; Pair every colour state with an icon or label +- **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only +- **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. +- **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself +- **Also stated as:** modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group). + +## 17. `craft.line-length-50-75-characters` Cap paragraph measure at 50–75 characters per line and do not let it fall under about 30. @@ -185,7 +198,7 @@ Cap paragraph measure at 50–75 characters per line and do not let it fall unde - **Unless:** Tables and forms are not paragraph text; Blurbs under 3 lines are not measured - **Also stated as:** canon.bringhurst-measure-45-75 (Robert Bringhurst). -## 17. `craft.center-text-only-under-three-lines` +## 18. `craft.center-text-only-under-three-lines` Centre text only when there are fewer than three lines, and rarely even then; body text is left-aligned. @@ -197,18 +210,6 @@ Centre text only when there are fewer than three lines, and rarely even then; bo - **Unless:** Poetry, quotes or hero copy the project explicitly commits to - **Also stated as:** canon.vignelli-flush-left (Massimo Vignelli). -## 18. `craft.wcag-contrast-and-dont-rely-on-colour-alone` - -Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. - -- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html -- **In their words:** "The WCAG recommended contrast ratio for body text — 4.5:1 to meet AA standards" -- **Do:** Style coloured badges as a soft background with dark text; Pair every colour state with an icon or label -- **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only -- **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. -- **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself -- **Also stated as:** modern.contrast-and-not-colour-alone (Apple). - ## 19. `craft.size-isnt-everything-use-weight-and-colour` Build text hierarchy from two or three colours and two weights; use size only for real levels. @@ -275,18 +276,7 @@ The leading is the vertical unit; add and remove vertical space in multiples of - **Look at:** Body line-height L; margins and paddings between text blocks as multiples of L (or L/2 if the lens allows). - **Unless:** More leading for longer measures, darker faces, larger x-height and sans serifs: the ratio moves with the face -## 25. `modern.cards-are-a-choice-not-a-default` - -Cards are for browsing; use lists for searching and tables for comparing, and keep each card readable with one primary action. - -- **Source:** Stan Kirilov, *UI Card Design (StanVision journal)* — https://www.stan.vision/journal/ui-card-design-examples-best-practices-and-common-patterns (secondary) -- **In their words:** "Cards for browsing. Lists for searching. Tables for comparing." -- **Do:** Lists for ranked or homogeneous data, tables for comparison; Consistent card heights; The title as the link, with a pseudo-element extending the hit area -- **Don't:** Card grids for search results and settings; Whole-card anchor wrappers; More than 3 clickable elements per card; Nested cards -- **Look at:** Detect card grids (3+ siblings with equal border, shadow and radius); fail on text under 14px, over 3 interactive descendants per card, row heights differing over 20%, or an anchor wrapping more than heading plus paragraph. -- **Unless:** Masonry where variable height is the design choice; Media browsing such as Netflix or Pinterest - -## 26. `modern.no-junk-drawer-menus-or-unlabeled-icons` +## 25. `modern.no-junk-drawer-menus-or-unlabeled-icons` Icons carry labels, menus are named for what they hold, and no primary feature hides under More or an ellipsis. @@ -298,7 +288,7 @@ Icons carry labels, menus are named for what they hold, and no primary feature h - **Unless:** Universally recognised icons in tight toolbars, though even the hamburger is weaker than designers think - **Also stated as:** usability.dont-make-me-think (Steve Krug). -## 27. `modern.interactions-feel-immediate-under-200ms` +## 26. `modern.interactions-feel-immediate-under-200ms` Interaction transitions run 200ms or less with an ease-out curve so the interface feels immediate. @@ -309,7 +299,7 @@ Interaction transitions run 200ms or less with an ease-out curve so the interfac - **Look at:** Computed transition-duration and animation-duration on elements that change on :hover, :focus, [aria-expanded] or [data-state]; flag any over 300ms, warn over 200ms; ease-in on an entering element fails. - **Unless:** Toasts may run slower with a plain ease on purpose for tone; Large page or scene transitions and decorative loops sit outside the interaction budget; Exit animations can be a bit more relaxed -## 28. `modern.motion-values-proportional-to-trigger` +## 27. `modern.motion-values-proportional-to-trigger` Scale and fade motion starts near its resting size, in proportion to the trigger, never from zero or a heavy squash. @@ -320,7 +310,7 @@ Scale and fade motion starts near its resting size, in proportion to the trigger - **Look at:** Parse @keyframes and WAAPI keyframes on dialogs, popovers and buttons and read the starting scale(); :active transforms below about 0.9 fail. - **Unless:** Elements that genuinely originate from a point, such as a FAB expanding into a sheet, can grow from small -## 29. `modern.animate-only-transform-and-opacity` +## 28. `modern.animate-only-transform-and-opacity` Animate only transform and opacity, listing properties explicitly; never transition all or animate layout properties. @@ -331,7 +321,7 @@ Animate only transform and opacity, listing properties explicitly; never transit - **Look at:** Scan stylesheets and computed transition-property for all and for layout properties; scan @keyframes for width, height, top and left. - **Unless:** Accordion height animation via grid-template-rows or interpolate-size is layout by nature and accepted when it is the only honest way -## 30. `modern.focus-is-visible-and-unobscured` +## 29. `modern.focus-is-visible-and-unobscured` Every focusable element shows a visible focus ring on :focus-visible, and no sticky or fixed element ever covers it. @@ -341,7 +331,7 @@ Every focusable element shows a visible focus ring on :focus-visible, and no sti - **Don't:** outline: none or 0 with no replacement; Focus rings hidden under sticky headers - **Look at:** Tab through every focusable element; diff the focused and unfocused rect or read computed outline and box-shadow under :focus-visible; check the focused rect is not intersected by position fixed or sticky elements above it. -## 31. `modern.numbers-and-text-do-not-shift-layout` +## 30. `modern.numbers-and-text-do-not-shift-layout` Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. @@ -352,7 +342,7 @@ Numbers in columns and timers use tabular figures, images carry dimensions, and - **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. - **Unless:** Prose numbers -## 32. `modern.defaults-are-decisions-you-inherited` +## 31. `modern.defaults-are-decisions-you-inherited` A library or AI default is someone else's decision; commit to one written aesthetic direction instead of inheriting it. @@ -362,3 +352,92 @@ A library or AI default is someone else's decision; commit to one written aesthe - **Don't:** Untouched library defaults; Clean and modern as a brief; The AI-look constellation of purple gradient, Inter, identical cards, glass glow and bounce hover - **Look at:** Score the constellation: purple-to-blue/cyan gradients warn, Inter or Roboto with no display face notes, 3+ identical icon+h3+p cards note, backdrop-filter plus glow notes, gradient text on numerals warns, overshoot cubic-bezier on hover warns. - **Unless:** A purple brand is allowed to be purple; the tell is the constellation and the absence of a decision, not any one colour + +## 32. `color.from-tokens-not-hex` + +Every colour on the page comes from the design system's named tokens or palette functions, never from hex values copied into components. + +- **Source:** GOV.UK Design System (Government Digital Service), *Styles: Colour* — https://design-system.service.gov.uk/styles/colour/ +- **In their words:** "Do not copy the specific hexadecimal (hex) colour values." +- **Do:** Reference colour by role token (brand, text, error, border) rather than by value; Use palette colours (tints and shades of a few families) for supporting elements; Use a functional token only in the context it is designed for +- **Don't:** Hex literals in component styles; Near-duplicate colours (#1d70b8 next to #1d70b9) created by eye-dropping; Using the error token as a general red +- **Look at:** Collect every computed color, background-color, border-*-color, outline-color and fill/stroke of painted elements, and every value of CSS custom properties declared on :root (and on any theme selector). Count distinct painted colours that match no custom-property value (exact RGBA after resolution), and count pairs of painted colours closer than ΔE 2 that are not identical. +- **Unless:** Images, illustrations and embedded third-party widgets; Browser defaults on unstyled native controls; GOV.UK: palette colours (not functional ones) are allowed for illustrations and custom components + +## 33. `color.one-action-colour-apart-from-status` + +Links and primary actions share one interactive colour family, used consistently, and that colour is not the colour of errors, warnings or success. + +- **Source:** IBM Carbon Design System, *Elements: Color, Overview (Color anatomy)* — https://carbondesignsystem.com/elements/color/overview/ +- **In their words:** "The core blue family serves as the primary action color across all IBM products and experiences. Additional colors are used sparingly and purposefully." +- **Do:** One link colour, used only for links; Primary buttons in the brand or action colour; Danger colour reserved for destructive buttons, not for the primary action +- **Don't:** Links in the error red; Primary buttons in several different hues across screens; Static text coloured like links +- **Look at:** Take the computed text colour of every a[href] in running text and the background of every primary (first, filled, or type=submit) button. Count distinct hues among them (30° bins), and count links or primary buttons whose colour is within ΔE 10 of the page's error, warning or success colour. Also count non-interactive text elements painted in the link colour. +- **Unless:** Primer (GitHub) deliberately uses the success role for primary buttons; a system that states such a mapping consistently is following its own rule; Destructive primary actions (Delete account) take the danger colour on purpose; Navigation menus, where position signals the link + +## 34. `color.focus-ring-contrasts-with-its-surroundings` + +The focus indicator contrasts at least 3:1 with whatever it is drawn against: the page background for an outer ring, the component's own colours for an inner one. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast, Relationship with Focus Visible (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." +- **Do:** An outer ring that contrasts with the page background; A two-colour ring (dark and light) that holds on any background; A thick indicator rather than a 1px one +- **Don't:** A yellow outer ring on a white page; A focus border that changes hue inside the component without contrasting with its fill; Focus shown only by a background tint change +- **Look at:** Tab to each focusable control; diff the focused and unfocused screenshots of the control's box padded by a few pixels; for the changed pixels, take their colour and the colour of the unchanged pixels adjacent to them (page background outside, component fill inside). Count controls where no changed region reaches 3:1 against its adjacent colour. page.focus-visible checks only that some pixel changes; this checks that the change can be seen. +- **Unless:** Unmodified browser default focus styles; WCAG does not compare focused and unfocused states with each other; a background-only change is out of scope for 1.4.11 but fails Use of Color + +## 35. `color.few-families-in-proportion` + +Use a few colour families in a deliberate proportion (neutral base dominant, then primary, secondary and a small accent), not an even spread of many hues. + +- **Source:** U.S. Web Design System (GSA), *Design tokens: Theme color tokens* — https://designsystem.digital.gov/design-tokens/color/theme-tokens/ +- **In their words:** "about 60% of your site’s color would be the primary color family, about 30% would be the secondary color family, and about 10% would be the accent color families" +- **Do:** Neutral base for text and most surfaces; One primary family carrying most of the colour, one secondary, a small accent; Additional colours used sparingly and for a purpose (Carbon); Start in black and white, then add colour to support the message (USWDS) +- **Don't:** Five or more saturated hue families at similar weight on one screen; An accent that covers more area than the primary; A new hue introduced for a single component +- **Look at:** Screenshot the page and bucket every non-neutral pixel (HSL saturation above about 15%) by hue into 30° bins, ignoring images and status colours. How many hue families take more than 2% of the coloured area, and does the largest one carry most of it while the smallest (accent) stays near a tenth? +- **Unless:** USWDS: the proportions are for non-base colours; neutral text will usually dominate; Illustration, photography and data visualisation, which need their own palettes; Brands whose identity is multi-hue + +## 36. `writing.sentence-case-ui-text` + +Labels, buttons, headings, tabs and table headers are written in sentence case; capitals only for the first word, proper nouns and acronyms — never title case or all caps for emphasis. + +- **Source:** IBM Carbon Design System, *Content guidelines: Writing style, Capitalization* — https://carbondesignsystem.com/guidelines/content/writing-style/ +- **In their words:** "Use sentence–case capitalization for all UI text elements." +- **Do:** 'First name', 'Email address', 'Save changes'; Refer to a UI element with the capitalization it has in the UI ('the My network page'); Use bold or italic, not capitals, for emphasis +- **Don't:** Title Case Buttons Like 'Save Your Changes'; ALL CAPS text set in the markup (as opposed to a tracked small label styled with text-transform); Capitalizing feature names to mark them as special +- **Look at:** For each button, label, th, tab and h1–h6 with three or more words, take the rendered text (after text-transform) and count those where two or more non-initial words that are not acronyms or in the page's proper-noun list start with a capital (title case), or where the whole text of four or more words is upper case. Count must be 0. +- **Unless:** Proper nouns, product names and acronyms keep their capitals; Mailchimp's own guide uses title case for page titles, menu names and global navigation — a house style, decide one and keep it; Short tracked overline labels in caps are a typographic choice; the four-word floor leaves them alone + +## 37. `writing.dates-numbers-units-for-the-reader` + +Dates spell out the month, numbers are numerals with thousands separators, and units sit a space after their number — formatted in the reader's locale, never as an ambiguous all-numeric date or a raw machine value. + +- **Source:** Shopify Polaris, *Content: Grammar and mechanics, 'Numbers, dates, and currency'* — https://polaris.shopify.com/content/grammar-and-mechanics +- **In their words:** "Use the month’s full name. If there isn’t enough space, use 3-letter abbreviations. Don’t write dates with numerals only." +- **Do:** 'December 11, 2024' or 'Dec 11, 2024' (in the reader's locale order); Numerals, not words: 'You have 5 orders to fulfill'; Thousands separators: '12,000'; A space between number and unit: '3.4 lb', '2 kg'; Currency code after the amount when currencies can be confused: '$10,000 USD'; Format with Intl.DateTimeFormat / Intl.NumberFormat for the user's locale +- **Don't:** All-numeric dates like '12/11/24'; ISO timestamps or epoch values shown raw ('2024-12-11T09:30:00Z'); Ordinals in dates ('January 23rd'); Unit glued to the number ('3.4lb'); Shortened numbers like '12 k' where the exact value matters +- **Look at:** Scan visible text nodes. Count matches of all-numeric dates (\b\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}\b), raw ISO timestamps (\d{4}-\d{2}-\d{2}T\d{2}:), integers of 5+ digits with no separator outside codes/IDs, and numbers glued to a unit (\d(kg|lb|cm|mm|km|mi|ml|oz)\b). Count must be 0. +- **Unless:** Polaris notes these are American English base rules and dates, numbers and measurements should be localized automatically — the target is the reader's locale, not US format; Identifiers, codes, SKUs and years are not quantities and take no separator; Dense data tables may use compact numeric dates if the format is unambiguous for the locale and stated in the column header + +## 38. `data-display.no-tables-for-layout` + +A table is for comparing data in rows and columns, never for arranging content on the page; layout belongs to the grid. + +- **Source:** GOV.UK Design System (Government Digital Service), *Table — When not to use this component* — https://design-system.service.gov.uk/components/table/ +- **In their words:** "Never use the table component to layout content on a page." +- **Do:** CSS grid or flex for page and dashboard layout; A list, cards or a summary list for items that do not share columns; Tables only where every row has the same fields +- **Don't:** A table that positions a sidebar, form or dashboard tiles; Table cells holding headings, paragraphs or whole forms; role=presentation on a table that actually holds tabular data +- **Look at:** Count table elements that have no th and either contain headings, more than one paragraph per cell, form fieldsets or nested tables, or have a single row whose cells hold unrelated content blocks. +- **Unless:** HTML email, where tables are still the only reliable layout tool +- **Also stated as:** modern.cards-are-a-choice-not-a-default (Stan Kirilov). + +## 39. `data-display.header-cells-are-th-with-scope` + +A data table marks its header cells as th and its data cells as td; when headers run both across and down, scope (or id/headers in complex tables) ties each cell to its headers. + +- **Source:** W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.), *Tables Tutorial* — https://www.w3.org/WAI/tutorials/tables/ +- **In their words:** "Header cells must be marked up with <th>, and data cells with <td> to make tables accessible. For more complex tables, explicit associations may be needed using scope, id, and headers attributes." +- **Do:** A thead row of th scope=col in every data table; th scope=row for the first cell when it names the row's item; id and headers attributes when a cell sits under more than one level of header; A caption that names the table +- **Don't:** Header rows built from td styled bold; Div grids that look like tables with no role=table/row/columnheader/cell semantics; One table holding several topics separated by extra rows of th +- **Look at:** Count table (and role=table/grid) elements that have more than one row of data and no th / role=columnheader; count tables with both a header row and a header column whose th lack scope; count tables with no caption, aria-label or aria-labelledby. +- **Unless:** A simple table with one header row needs only th, not scope; scope matters once there are header rows and header columns; Layout tables, which should not exist (see data-display.no-tables-for-layout) diff --git a/skills/uxcli/lenses/data.json b/skills/uxcli/lenses/data.json index 4ebb6eb..2935252 100644 --- a/skills/uxcli/lenses/data.json +++ b/skills/uxcli/lenses/data.json @@ -4,9 +4,13 @@ "name": "Data and dashboards", "when": "Dashboards, analytics, monitoring and reports: numbers and charts read again and again.", "picks": [ + { + "viewpoint": "usability.visibility-of-system-status", + "note": "Same rule, also stated as: modern.feedback-is-local-and-optimistic (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn); writing.success-names-what-happened (Shopify Polaris)." + }, { "viewpoint": "usability.feedback-within-a-second", - "note": "Same rule, also stated as: modern.interactions-feel-immediate-under-200ms (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn)." + "note": "Same rule, also stated as: modern.interactions-feel-immediate-under-200ms (Rauno Freiberg)." }, { "viewpoint": "usability.signifiers-make-clickable-look-clickable", @@ -17,11 +21,16 @@ "note": "Same rule, also stated as: craft.tap-targets-and-control-height (Erik D. Kennedy); modern.hit-targets-meet-platform-minimums (Apple)." }, { - "viewpoint": "usability.visibility-of-system-status", - "note": "Same rule, also stated as: modern.feedback-is-local-and-optimistic (Rauno Freiberg)." + "viewpoint": "usability.speak-the-users-language", + "note": "Same rule, also stated as: writing.errors-say-what-and-how-to-fix (GOV.UK Design System (Government Digital Service))." }, { - "viewpoint": "usability.speak-the-users-language" + "viewpoint": "usability.omit-needless-words", + "note": "Same rule, also stated as: writing.sentence-case-ui-text (IBM Carbon Design System)." + }, + { + "viewpoint": "usability.clearly-marked-emergency-exit", + "note": "Same rule, also stated as: writing.destructive-actions-name-the-consequence (IBM Carbon Design System)." }, { "viewpoint": "usability.no-false-affordances" @@ -30,15 +39,9 @@ "viewpoint": "usability.natural-mapping-and-proximity", "note": "Same rule, also stated as: craft.law-of-locality (Erik D. Kennedy)." }, - { - "viewpoint": "usability.omit-needless-words" - }, { "viewpoint": "usability.aesthetic-usability-effect-bias" }, - { - "viewpoint": "usability.clearly-marked-emergency-exit" - }, { "viewpoint": "usability.start-with-user-needs-design-with-data" }, @@ -46,23 +49,27 @@ "viewpoint": "craft.fewer-borders", "note": "Same rule, also stated as: canon.gestalt-common-region (Aurora Harley, NN/g); canon.rams-as-little-design-as-possible (Dieter Rams); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); canon.tufte-smallest-effective-difference (Edward Tufte); canon.tufte-one-plus-one-equals-three (Edward Tufte)." }, + { + "viewpoint": "craft.dont-overlook-empty-states-teach-by-example", + "note": "Same rule, also stated as: modern.every-state-is-designed (Vercel Labs); writing.empty-states-say-what-next (GitHub Primer); data-display.empty-data-explains-and-offers-next-step (IBM Carbon Design System); data-display.loading-skeleton-holds-the-layout (GitHub Primer); data-display.wrap-before-truncating-and-reveal-the-rest (GitHub Primer)." + }, { "viewpoint": "craft.spacing-and-sizing-system", "note": "Same rule, also stated as: canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen); modern.spacing-comes-from-a-scale (Stan Kirilov)." }, { - "viewpoint": "craft.separation-order-space-then-lines-then-boxes" + "viewpoint": "craft.wcag-contrast-and-dont-rely-on-colour-alone", + "note": "Same rule, also stated as: modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group); color.chart-marks-readable-without-hue (GitHub Primer)." }, { - "viewpoint": "craft.de-emphasize-to-emphasize-up-pop-down-pop" + "viewpoint": "craft.separation-order-space-then-lines-then-boxes" }, { - "viewpoint": "craft.wcag-contrast-and-dont-rely-on-colour-alone", - "note": "Same rule, also stated as: modern.contrast-and-not-colour-alone (Apple)." + "viewpoint": "craft.align-with-readability-in-mind", + "note": "Same rule, also stated as: data-display.right-align-numbers-tabular-figures (GitHub Primer); data-display.text-left-headers-follow-their-column (W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.))." }, { - "viewpoint": "craft.dont-overlook-empty-states-teach-by-example", - "note": "Same rule, also stated as: modern.every-state-is-designed (Vercel Labs)." + "viewpoint": "craft.de-emphasize-to-emphasize-up-pop-down-pop" }, { "viewpoint": "craft.hierarchy-is-everything-squint-test" @@ -73,34 +80,24 @@ { "viewpoint": "craft.type-scale-few-font-sizes" }, - { - "viewpoint": "craft.align-with-readability-in-mind" - }, { "viewpoint": "canon.bringhurst-leading-is-a-rhythmic-unit", "note": "Same rule, also stated as: canon.mb-grid-fields-from-columns-and-lines (Josef Müller-Brockmann)." }, - { - "viewpoint": "canon.tufte-data-ink-ratio", - "note": "Same rule, also stated as: canon.tufte-forgo-chartjunk (Edward Tufte)." - }, { "viewpoint": "canon.tufte-small-multiples" }, - { - "viewpoint": "canon.tufte-graphical-integrity" - }, { "viewpoint": "modern.no-junk-drawer-menus-or-unlabeled-icons", - "note": "Same rule, also stated as: usability.dont-make-me-think (Steve Krug); craft.labels-are-a-last-resort (Steve Schoger (attendee notes by ynotdraw))." - }, - { - "viewpoint": "modern.cards-are-a-choice-not-a-default" + "note": "Same rule, also stated as: writing.buttons-name-the-action (IBM Carbon Design System); usability.dont-make-me-think (Steve Krug); craft.labels-are-a-last-resort (Steve Schoger (attendee notes by ynotdraw))." }, { "viewpoint": "modern.frequent-actions-do-not-animate", "note": "Same rule, also stated as: modern.delight-scales-with-rarity (Benji Taylor)." }, + { + "viewpoint": "modern.numbers-and-text-do-not-shift-layout" + }, { "viewpoint": "modern.motion-values-proportional-to-trigger" }, @@ -122,9 +119,6 @@ { "viewpoint": "modern.focus-is-visible-and-unobscured" }, - { - "viewpoint": "modern.numbers-and-text-do-not-shift-layout" - }, { "viewpoint": "modern.shadows-share-one-light-source" }, @@ -136,6 +130,52 @@ }, { "viewpoint": "modern.defaults-are-decisions-you-inherited" + }, + { + "viewpoint": "color.from-tokens-not-hex", + "note": "Same rule, also stated as: color.mode-aware-tokens-in-every-theme (GitHub Primer)." + }, + { + "viewpoint": "color.controls-and-graphics-3-to-1" + }, + { + "viewpoint": "color.status-colours-keep-their-meaning" + }, + { + "viewpoint": "color.one-action-colour-apart-from-status" + }, + { + "viewpoint": "color.focus-ring-contrasts-with-its-surroundings" + }, + { + "viewpoint": "writing.one-label-per-action" + }, + { + "viewpoint": "writing.dates-numbers-units-for-the-reader" + }, + { + "viewpoint": "data-display.no-tables-for-layout", + "note": "Same rule, also stated as: modern.cards-are-a-choice-not-a-default (Stan Kirilov)." + }, + { + "viewpoint": "data-display.chart-axes-and-bars-are-labelled", + "note": "Same rule, also stated as: canon.tufte-data-ink-ratio (Edward Tufte); canon.tufte-forgo-chartjunk (Edward Tufte)." + }, + { + "viewpoint": "data-display.chart-axes-are-honest", + "note": "Same rule, also stated as: color.chart-palette-fits-the-data (IBM Carbon Design System); canon.tufte-graphical-integrity (Edward Tufte)." + }, + { + "viewpoint": "data-display.one-unit-and-format-per-column" + }, + { + "viewpoint": "data-display.header-cells-are-th-with-scope" + }, + { + "viewpoint": "data-display.long-tables-sort-and-say-so" + }, + { + "viewpoint": "data-display.summary-list-for-key-value-facts" } ] } diff --git a/skills/uxcli/lenses/data.md b/skills/uxcli/lenses/data.md index 567dbe9..277d16b 100644 --- a/skills/uxcli/lenses/data.md +++ b/skills/uxcli/lenses/data.md @@ -2,9 +2,21 @@ Dashboards, analytics, monitoring and reports: numbers and charts read again and again. -40 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. +51 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. -## 1. `usability.feedback-within-a-second` +## 1. `usability.visibility-of-system-status` + +Every action with consequences shows the user something changed, as quickly as possible, so they always know what the system is doing. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #1* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "The design should always keep users informed about what is going on, through appropriate feedback within a reasonable amount of time." +- **Do:** Show a visible trace of every state change; Give feedback immediately, or as quickly as possible; Disable and label a control while its action runs +- **Don't:** Submit silently; Change state with no visible trace; Show an action's result only somewhere else +- **Look at:** Drive an action, then diff DOM or pixels over the next frames: did anything visibly change within 1 s; is there a live region, status text, spinner or busy state? +- **Unless:** Below 0.1 s no special feedback is needed beyond showing the result +- **Also stated as:** modern.feedback-is-local-and-optimistic (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn); writing.success-names-what-happened (Shopify Polaris). + +## 2. `usability.feedback-within-a-second` Paint something within 0.1 s; keep the user's flow with a response under 1 s; show a progress indicator for anything longer. @@ -14,9 +26,9 @@ Paint something within 0.1 s; keep the user's flow with a response under 1 s; sh - **Don't:** Leave dead time with no indicator; Show a percent-done bar that lies badly - **Look at:** Time from input event to first paint of any change; presence of a progress element, aria-busy, <progress> or role=status update when the wait exceeds 1 s. - **Unless:** Laws of UX claims a purposeful delay can raise perceived value — an opinion with no study cited -- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn). +- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg). -## 2. `usability.signifiers-make-clickable-look-clickable` +## 3. `usability.signifiers-make-clickable-look-clickable` Links and buttons must look clickable — colour, underline, border or fill — because weak signifiers cost measured time and fixations. @@ -28,7 +40,7 @@ Links and buttons must look clickable — colour, underline, border or fill — - **Unless:** Link position in nav menus or peripheral lists may eliminate the need for underlining (Loranger); Flat works best with low information density, traditional layouts and high-contrast targets positioned standardly (Moran) - **Also stated as:** modern.signifiers-survive-flatness (Kate Moran (NN/G)); modern.no-dead-zones-and-honest-clickability (Rauno Freiberg). -## 3. `usability.fitts-target-size-and-distance` +## 4. `usability.fitts-target-size-and-distance` Make targets big, space them apart, and put them where the pointer already is; touch targets at least 1 cm square. @@ -40,18 +52,6 @@ Make targets big, space them apart, and put them where the pointer already is; t - **Unless:** Infinite targets along screen edges for mouse — size matters less at an edge; Primary CTAs, moving users, children and the elderly need larger than the minimum - **Also stated as:** craft.tap-targets-and-control-height (Erik D. Kennedy); modern.hit-targets-meet-platform-minimums (Apple). -## 4. `usability.visibility-of-system-status` - -Every action with consequences shows the user something changed, as quickly as possible, so they always know what the system is doing. - -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #1* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "The design should always keep users informed about what is going on, through appropriate feedback within a reasonable amount of time." -- **Do:** Show a visible trace of every state change; Give feedback immediately, or as quickly as possible; Disable and label a control while its action runs -- **Don't:** Submit silently; Change state with no visible trace; Show an action's result only somewhere else -- **Look at:** Drive an action, then diff DOM or pixels over the next frames: did anything visibly change within 1 s; is there a live region, status text, spinner or busy state? -- **Unless:** Below 0.1 s no special feedback is needed beyond showing the result -- **Also stated as:** modern.feedback-is-local-and-optimistic (Rauno Freiberg). - ## 5. `usability.speak-the-users-language` Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. @@ -62,8 +62,33 @@ Write in the words the actor already uses; no internal jargon, error codes or un - **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations - **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. - **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se +- **Also stated as:** writing.errors-say-what-and-how-to-fix (GOV.UK Design System (Government Digital Service)). + +## 6. `usability.omit-needless-words` + +Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. + +- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) +- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" +- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns +- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case +- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. +- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text +- **Also stated as:** writing.sentence-case-ui-text (IBM Carbon Design System). + +## 7. `usability.clearly-marked-emergency-exit` + +Every interaction has a visible, labelled way out — Cancel, Back, Escape, Undo — so a mistaken action does not trap the user. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #3* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "Users often perform actions by mistake. They need a clearly marked 'emergency exit' to leave the unwanted action without having to go through an extended process." +- **Do:** Support Undo and Redo; Show a clear Cancel or close control; Label the exit clearly and make it discoverable +- **Don't:** Open a modal with no close; Build a wizard step with no back; Ship a destructive action with no undo +- **Look at:** For each role=dialog or modal: a focusable control whose text or aria-label matches close/cancel/back, and Escape dismisses it; for each step past the first in a flow: a back control exists. +- **Unless:** Legally required interstitials — the exit must still be visible, not necessarily free of consequence +- **Also stated as:** writing.destructive-actions-name-the-consequence (IBM Carbon Design System). -## 6. `usability.no-false-affordances` +## 8. `usability.no-false-affordances` Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. @@ -73,7 +98,7 @@ Nothing that is not interactive may look interactive: no underlined or blue stat - **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons - **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). -## 7. `usability.natural-mapping-and-proximity` +## 9. `usability.natural-mapping-and-proximity` Put a control next to the thing it changes and a label next to its field; gaps between groups must exceed gaps within them. @@ -85,18 +110,7 @@ Put a control next to the thing it changes and a label next to its field; gaps b - **Unless:** Left-aligned labels are acceptable if space is constrained and labels are of similar length - **Also stated as:** craft.law-of-locality (Erik D. Kennedy). -## 8. `usability.omit-needless-words` - -Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. - -- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) -- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" -- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns -- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case -- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. -- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text - -## 9. `usability.aesthetic-usability-effect-bias` +## 10. `usability.aesthetic-usability-effect-bias` Polish makes a screen look more usable than it is; discount your aesthetic impression and run the measurable checks first. @@ -107,17 +121,6 @@ Polish makes a screen look more usable than it is; discount your aesthetic impre - **Look at:** Did the evaluator judge the screen usable because it looks good? Re-check the verdict against the count-kind entries before trusting it; 'attractive' is the variable and stays taste. - **Unless:** With severe usability issues, or functionality sacrificed for aesthetics, users lose patience -## 10. `usability.clearly-marked-emergency-exit` - -Every interaction has a visible, labelled way out — Cancel, Back, Escape, Undo — so a mistaken action does not trap the user. - -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #3* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "Users often perform actions by mistake. They need a clearly marked 'emergency exit' to leave the unwanted action without having to go through an extended process." -- **Do:** Support Undo and Redo; Show a clear Cancel or close control; Label the exit clearly and make it discoverable -- **Don't:** Open a modal with no close; Build a wizard step with no back; Ship a destructive action with no undo -- **Look at:** For each role=dialog or modal: a focusable control whose text or aria-label matches close/cancel/back, and Escape dismisses it; for each step past the first in a flow: a back control exists. -- **Unless:** Legally required interstitials — the exit must still be visible, not necessarily free of consequence - ## 11. `usability.start-with-user-needs-design-with-data` Name the actor and their need before drawing a screen, then let measured behaviour, not hunches or looks, decide what changes. @@ -140,7 +143,19 @@ Separate elements with space, a background shift, a shadow or striping before re - **Unless:** Form inputs and the one object a person acts on keep their border; Keylines that make disconnected content feel connected; Decorative borders as style once the count is low - **Also stated as:** canon.gestalt-common-region (Aurora Harley, NN/g); canon.rams-as-little-design-as-possible (Dieter Rams); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); canon.tufte-smallest-effective-difference (Edward Tufte); canon.tufte-one-plus-one-equals-three (Edward Tufte). -## 13. `craft.spacing-and-sizing-system` +## 13. `craft.dont-overlook-empty-states-teach-by-example` + +Design the first-load and empty states with sample data or an example, a message and a call to action; never a blank page. + +- **Source:** Erik D. Kennedy, *4 Rules for Intuitive UX* — https://www.learnui.design/blog/4-rules-intuitive-ux.html +- **In their words:** "Use the 'first load' experience to provide sample data, showing by example what the properly-working app will look like" +- **Do:** Show a designed empty state with message, example and call to action; Seed sample data on first load; Show examples rather than descriptions +- **Don't:** Show a totally blank page on first load; Show a table header with no rows and no message +- **Look at:** In the journey's declared empty state, find list or table containers with 0 data children; require a visible sentence inside or adjacent and at least one actionable element. +- **Unless:** Zero-hit search results need a message but no sample data; Transient loading states +- **Also stated as:** modern.every-state-is-designed (Vercel Labs); writing.empty-states-say-what-next (GitHub Primer); data-display.empty-data-explains-and-offers-next-step (IBM Carbon Design System); data-display.loading-skeleton-holds-the-layout (GitHub Primer); data-display.wrap-before-truncating-and-reveal-the-rest (GitHub Primer). + +## 14. `craft.spacing-and-sizing-system` Draw every margin, padding and gap from one scale, and make space between groups clearly larger than space within them. @@ -152,7 +167,19 @@ Draw every margin, padding and gap from one scale, and make space between groups - **Unless:** Optical adjustments on icons and hanging punctuation sit a pixel or two off-scale - **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen); modern.spacing-comes-from-a-scale (Stan Kirilov). -## 14. `craft.separation-order-space-then-lines-then-boxes` +## 15. `craft.wcag-contrast-and-dont-rely-on-colour-alone` + +Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. + +- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html +- **In their words:** "The WCAG recommended contrast ratio for body text — 4.5:1 to meet AA standards" +- **Do:** Style coloured badges as a soft background with dark text; Pair every colour state with an icon or label +- **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only +- **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. +- **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself +- **Also stated as:** modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group); color.chart-marks-readable-without-hue (GitHub Primer). + +## 16. `craft.separation-order-space-then-lines-then-boxes` Use the lightest separator that works: more space first, then a keyline or background band, and a box only for the object that is acted on. @@ -162,7 +189,19 @@ Use the lightest separator that works: more space first, then a keyline or backg - **Look at:** Between sibling groups, record which separator is used: gap at least 2x the inner gap (space), hr or border-bottom (line), bordered or shadowed wrapper (box); report the inner/outer gap ratio. - **Unless:** Dense data such as tables where zebra stripes or keylines are used; Interactive cards that are the unit of action -## 15. `craft.de-emphasize-to-emphasize-up-pop-down-pop` +## 17. `craft.align-with-readability-in-mind` + +Keep few strong left edges, right-align comparable numbers with tabular figures, and hang punctuation so edges line up. + +- **Source:** Steve Schoger, *Little UI Details (tweet, 15 Jun 2017)* — https://digitalsynopsis.com/design/useful-ui-ux-design-tips/ +- **In their words:** "Aligning text is an easy way to clean up your design and make your content much more scannable." +- **Do:** Keep few, strong left edges; Right-align numeric columns with tabular figures; Hang bullets, icons and punctuation so text edges align +- **Don't:** Align a right-aligned image to ragged left-aligned text; Centre columns of numbers +- **Look at:** Count distinct left-edge x positions of text blocks in a container at 1px tolerance; for td cells matching a number or currency pattern, text-align must be right or end and tabular figures must be set. +- **Unless:** IDs, ZIP codes and phone numbers stay left-aligned; Centred layouts under 3 lines +- **Also stated as:** data-display.right-align-numbers-tabular-figures (GitHub Primer); data-display.text-left-headers-follow-their-column (W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.)). + +## 18. `craft.de-emphasize-to-emphasize-up-pop-down-pop` To emphasize, quiet the competitors as much as you loudify the hero; never stack every up-pop property on non-title elements. @@ -173,31 +212,7 @@ To emphasize, quiet the competitors as much as you loudify the hero; never stack - **Look at:** Flag text at 2x body size or more that is also font-weight 700+, full contrast and uppercase, when it is not the page h1. - **Unless:** The page title may be all-out up-pop -## 16. `craft.wcag-contrast-and-dont-rely-on-colour-alone` - -Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. - -- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html -- **In their words:** "The WCAG recommended contrast ratio for body text — 4.5:1 to meet AA standards" -- **Do:** Style coloured badges as a soft background with dark text; Pair every colour state with an icon or label -- **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only -- **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. -- **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself -- **Also stated as:** modern.contrast-and-not-colour-alone (Apple). - -## 17. `craft.dont-overlook-empty-states-teach-by-example` - -Design the first-load and empty states with sample data or an example, a message and a call to action; never a blank page. - -- **Source:** Erik D. Kennedy, *4 Rules for Intuitive UX* — https://www.learnui.design/blog/4-rules-intuitive-ux.html -- **In their words:** "Use the 'first load' experience to provide sample data, showing by example what the properly-working app will look like" -- **Do:** Show a designed empty state with message, example and call to action; Seed sample data on first load; Show examples rather than descriptions -- **Don't:** Show a totally blank page on first load; Show a table header with no rows and no message -- **Look at:** In the journey's declared empty state, find list or table containers with 0 data children; require a visible sentence inside or adjacent and at least one actionable element. -- **Unless:** Zero-hit search results need a message but no sample data; Transient loading states -- **Also stated as:** modern.every-state-is-designed (Vercel Labs). - -## 18. `craft.hierarchy-is-everything-squint-test` +## 19. `craft.hierarchy-is-everything-squint-test` Squint: the most important thing must catch the eye first and the least important last; one element dominates each screen. @@ -208,7 +223,7 @@ Squint: the most important thing must catch the eye first and the least importan - **Look at:** Given a declared primary action, score every interactive element as area x contrast x font-weight factor; the declared primary must rank first, and no set of buttons may sit within 10% of each other. - **Unless:** Page titles are the only element styled all-out up-pop; A browsing page such as a gallery may have no single most important thing -## 19. `craft.greys-dont-have-to-be-grey-never-use-black` +## 20. `craft.greys-dont-have-to-be-grey-never-use-black` Tint the neutral scale toward the brand hue and never use pure black for text. @@ -219,7 +234,7 @@ Tint the neutral scale toward the brand hue and never use pure black for text. - **Look at:** Count text, border and background colours with saturation 0 and lightness under 15%, and the share of neutral swatches with saturation exactly 0. - **Unless:** It does not always work; worth a trial; High-contrast or accessibility modes and print -## 20. `craft.type-scale-few-font-sizes` +## 21. `craft.type-scale-few-font-sizes` Use about four font sizes from a fixed scale; reuse the default size for body, menus, lists and controls. @@ -230,17 +245,6 @@ Use about four font sizes from a fixed scale; reuse the default size for body, m - **Look at:** Count distinct computed font-size values on visible text per viewport and how many fall outside the declared scale. - **Unless:** Sizes must stay distinguishable, so large text may step many points apart; Marketing pages may add a display size -## 21. `craft.align-with-readability-in-mind` - -Keep few strong left edges, right-align comparable numbers with tabular figures, and hang punctuation so edges line up. - -- **Source:** Steve Schoger, *Little UI Details (tweet, 15 Jun 2017)* — https://digitalsynopsis.com/design/useful-ui-ux-design-tips/ -- **In their words:** "Aligning text is an easy way to clean up your design and make your content much more scannable." -- **Do:** Keep few, strong left edges; Right-align numeric columns with tabular figures; Hang bullets, icons and punctuation so text edges align -- **Don't:** Align a right-aligned image to ragged left-aligned text; Centre columns of numbers -- **Look at:** Count distinct left-edge x positions of text blocks in a container at 1px tolerance; for td cells matching a number or currency pattern, text-align must be right or end and tabular figures must be set. -- **Unless:** IDs, ZIP codes and phone numbers stay left-aligned; Centred layouts under 3 lines - ## 22. `canon.bringhurst-leading-is-a-rhythmic-unit` The leading is the vertical unit; add and remove vertical space in multiples of it. @@ -253,19 +257,7 @@ The leading is the vertical unit; add and remove vertical space in multiples of - **Unless:** More leading for longer measures, darker faces, larger x-height and sans serifs: the ratio moves with the face - **Also stated as:** canon.mb-grid-fields-from-columns-and-lines (Josef Müller-Brockmann). -## 23. `canon.tufte-data-ink-ratio` - -Show the data; erase ink that does not change when the data changes, and ink that repeats it. - -- **Source:** Edward Tufte, *The Visual Display of Quantitative Information (1983), pp.106–121* — https://guypursey.com/blog/202001041530-tufte-principles-visual-display-quantitative-information (secondary) -- **In their words:** "Above all else show the data. Maximize the data-ink ratio. Erase non-data-ink. Erase redundant data-ink. Revise and edit." -- **Do:** Label values directly on marks; Keep gridlines few and lighter than data -- **Don't:** Gridlines heavier than data; Boxed plot areas and redundant axes; Legends that repeat labels; One value said four ways -- **Look at:** In an SVG/canvas chart: non-data elements (gridlines, frames, ticks, legend entries) versus data elements (bars, points, path segments); stroke weight of gridlines versus data marks. -- **Unless:** Maximize the data-ink ratio, within reason; Principles should not be applied rigidly; better to violate any than place graceless marks -- **Also stated as:** canon.tufte-forgo-chartjunk (Edward Tufte). - -## 24. `canon.tufte-small-multiples` +## 23. `canon.tufte-small-multiples` Compare with repeated same-scale panels, not one overloaded chart. @@ -276,18 +268,7 @@ Compare with repeated same-scale panels, not one overloaded chart. - **Look at:** For sibling charts: equal size, equal axis domain (tick labels or scale attributes), same mark encoding. For one chart: number of overlaid series (more than 5 is a candidate). - **Unless:** Different domains are legitimate when the comparison is of shape, not magnitude (inference, not Tufte's text) -## 25. `canon.tufte-graphical-integrity` - -The size of a mark is directly proportional to the number it shows; bars start at zero; one visual dimension per data dimension. - -- **Source:** Edward Tufte, *The Visual Display of Quantitative Information (1983), graphical integrity* — https://guypursey.com/blog/202001041530-tufte-principles-visual-display-quantitative-information (secondary) -- **In their words:** "Show data variation, not design variation." -- **Do:** Bars from a zero baseline; Areas proportional to values -- **Don't:** Truncated bar axes; 3-D bars for 1-D data; Pictograms scaled in both width and height; Cherry-picked ranges -- **Look at:** Bar charts whose y-domain minimum is not 0; linear-fit residual of bar pixel heights against labelled values (lie factor); pictograms scaled in two dimensions. -- **Unless:** Line charts and dot plots may omit zero; the rule is about the representation being proportional - -## 26. `modern.no-junk-drawer-menus-or-unlabeled-icons` +## 24. `modern.no-junk-drawer-menus-or-unlabeled-icons` Icons carry labels, menus are named for what they hold, and no primary feature hides under More or an ellipsis. @@ -297,20 +278,9 @@ Icons carry labels, menus are named for what they hold, and no primary feature h - **Don't:** More, ellipsis or Tools catch-alls holding primary features; Icon-only toolbars without labels; Tooltips on disabled buttons - **Look at:** Icon-only buttons with no text and no aria-label fail; menus triggered by More, …, Tools or Options holding over 5 items or a primary-action label warn; disabled buttons with title or aria-describedby fail. - **Unless:** Universally recognised icons in tight toolbars, though even the hamburger is weaker than designers think -- **Also stated as:** usability.dont-make-me-think (Steve Krug); craft.labels-are-a-last-resort (Steve Schoger (attendee notes by ynotdraw)). +- **Also stated as:** writing.buttons-name-the-action (IBM Carbon Design System); usability.dont-make-me-think (Steve Krug); craft.labels-are-a-last-resort (Steve Schoger (attendee notes by ynotdraw)). -## 27. `modern.cards-are-a-choice-not-a-default` - -Cards are for browsing; use lists for searching and tables for comparing, and keep each card readable with one primary action. - -- **Source:** Stan Kirilov, *UI Card Design (StanVision journal)* — https://www.stan.vision/journal/ui-card-design-examples-best-practices-and-common-patterns (secondary) -- **In their words:** "Cards for browsing. Lists for searching. Tables for comparing." -- **Do:** Lists for ranked or homogeneous data, tables for comparison; Consistent card heights; The title as the link, with a pseudo-element extending the hit area -- **Don't:** Card grids for search results and settings; Whole-card anchor wrappers; More than 3 clickable elements per card; Nested cards -- **Look at:** Detect card grids (3+ siblings with equal border, shadow and radius); fail on text under 14px, over 3 interactive descendants per card, row heights differing over 20%, or an anchor wrapping more than heading plus paragraph. -- **Unless:** Masonry where variable height is the design choice; Media browsing such as Netflix or Pinterest - -## 28. `modern.frequent-actions-do-not-animate` +## 25. `modern.frequent-actions-do-not-animate` Actions used many times a day, and anything keyboard-triggered, appear instantly without an enter animation. @@ -322,7 +292,18 @@ Actions used many times a day, and anything keyboard-triggered, appear instantly - **Unless:** macOS context menus fade out and blink the chosen item; Rare features may be theatrical (delight-impact curve) - **Also stated as:** modern.delight-scales-with-rarity (Benji Taylor). -## 29. `modern.motion-values-proportional-to-trigger` +## 26. `modern.numbers-and-text-do-not-shift-layout` + +Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. + +- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ +- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" +- **Do:** tabular-nums in tables, timers and prices; width and height on every img +- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover +- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. +- **Unless:** Prose numbers + +## 27. `modern.motion-values-proportional-to-trigger` Scale and fade motion starts near its resting size, in proportion to the trigger, never from zero or a heavy squash. @@ -333,7 +314,7 @@ Scale and fade motion starts near its resting size, in proportion to the trigger - **Look at:** Parse @keyframes and WAAPI keyframes on dialogs, popovers and buttons and read the starting scale(); :active transforms below about 0.9 fail. - **Unless:** Elements that genuinely originate from a point, such as a FAB expanding into a sheet, can grow from small -## 30. `modern.animate-only-transform-and-opacity` +## 28. `modern.animate-only-transform-and-opacity` Animate only transform and opacity, listing properties explicitly; never transition all or animate layout properties. @@ -344,7 +325,7 @@ Animate only transform and opacity, listing properties explicitly; never transit - **Look at:** Scan stylesheets and computed transition-property for all and for layout properties; scan @keyframes for width, height, top and left. - **Unless:** Accordion height animation via grid-template-rows or interpolate-size is layout by nature and accepted when it is the only honest way -## 31. `modern.motion-has-an-origin` +## 29. `modern.motion-has-an-origin` A popover or menu animates from the side facing its trigger; set transform-origin where the motion physically starts. @@ -355,7 +336,7 @@ A popover or menu animates from the side facing its trigger; set transform-origi - **Look at:** For each open popover or menu with a known trigger (aria-controls or aria-haspopup), compare the computed transform-origin with the side facing the trigger. - **Unless:** Centred modals have no anchor; a centred origin is right there -## 32. `modern.honour-prefers-reduced-motion` +## 30. `modern.honour-prefers-reduced-motion` Every large motion has a reduced variant under prefers-reduced-motion; fade instead of slide rather than ignoring the setting. @@ -366,7 +347,7 @@ Every large motion has a reduced variant under prefers-reduced-motion; fade inst - **Look at:** Emulate prefers-reduced-motion: reduce and list elements whose computed animation-name or transition-property still includes translations over about 20px or durations above zero. - **Unless:** Motion that is the content, such as a video or a chart drawing, is out of scope -## 33. `modern.animations-are-interruptible` +## 31. `modern.animations-are-interruptible` An open or close animation can be reversed mid-flight by the next input; nothing waits for a transition to finish. @@ -377,7 +358,7 @@ An open or close animation can be reversed mid-flight by the next input; nothing - **Look at:** Open a panel, immediately send the close input, sample the element's bounding box about 50ms later; if it is still growing the animation was not interruptible. Depends on timing tolerance. - **Unless:** Destructive commits that fire only on gesture end are about triggering, not interruptibility -## 34. `modern.readable-type-sizes-and-weights` +## 32. `modern.readable-type-sizes-and-weights` Body text sits at the platform default size, weights stay 400 or heavier, and weight never changes on hover. @@ -388,7 +369,7 @@ Body text sits at the platform default size, weights stay 400 or heavier, and we - **Look at:** Computed font-size and font-weight of every text node at a phone viewport; count distinct font-family stacks and warn above 2. - **Unless:** Captions and legal text may sit at the platform minimum; Display headings may use light weights at large sizes -## 35. `modern.focus-is-visible-and-unobscured` +## 33. `modern.focus-is-visible-and-unobscured` Every focusable element shows a visible focus ring on :focus-visible, and no sticky or fixed element ever covers it. @@ -398,18 +379,7 @@ Every focusable element shows a visible focus ring on :focus-visible, and no sti - **Don't:** outline: none or 0 with no replacement; Focus rings hidden under sticky headers - **Look at:** Tab through every focusable element; diff the focused and unfocused rect or read computed outline and box-shadow under :focus-visible; check the focused rect is not intersected by position fixed or sticky elements above it. -## 36. `modern.numbers-and-text-do-not-shift-layout` - -Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. - -- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ -- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" -- **Do:** tabular-nums in tables, timers and prices; width and height on every img -- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover -- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. -- **Unless:** Prose numbers - -## 37. `modern.shadows-share-one-light-source` +## 34. `modern.shadows-share-one-light-source` All shadows on a page share one light direction and offset ratio, layered and tinted toward the background hue. @@ -420,7 +390,7 @@ All shadows on a page share one light direction and offset ratio, layered and ti - **Look at:** Collect every computed box-shadow; fail when offset signs mix across the page or distinct shadow strings exceed about 6; warn when shadow hue is far from the background hue with alpha above 0.5. - **Unless:** Inset shadows for sunken fields; Glows meant as glows -## 38. `modern.glass-and-blur-earn-their-place` +## 35. `modern.glass-and-blur-earn-their-place` Translucency is for chrome over busy backgrounds with heavy blur and a contrast fallback, never for body content or decoration. @@ -431,7 +401,7 @@ Translucency is for chrome over busy backgrounds with heavy blur and a contrast - **Look at:** For each element with backdrop-filter, sample contained-text contrast against the composited background at three scroll positions; any under 4.5:1 fails; blur under 12px over an image warns; body text over 2 lines inside glass warns. - **Unless:** OS-level materials that honour the user's Reduce Transparency setting -## 39. `modern.boring-and-familiar-beats-novel` +## 36. `modern.boring-and-familiar-beats-novel` Reuse known controls, gestures and semantics; a novel interaction needs a standard alternative and a reason. @@ -442,7 +412,7 @@ Reuse known controls, gestures and semantics; a novel interaction needs a standa - **Look at:** Custom widgets acting as select or menu with no role or keyboard handling fail; wheel or overscroll listeners calling preventDefault on document warn; drag-only affordances with no button alternative fail. - **Unless:** Deliberate departures with a point of view in place before the first component went down -## 40. `modern.defaults-are-decisions-you-inherited` +## 37. `modern.defaults-are-decisions-you-inherited` A library or AI default is someone else's decision; commit to one written aesthetic direction instead of inheriting it. @@ -452,3 +422,161 @@ A library or AI default is someone else's decision; commit to one written aesthe - **Don't:** Untouched library defaults; Clean and modern as a brief; The AI-look constellation of purple gradient, Inter, identical cards, glass glow and bounce hover - **Look at:** Score the constellation: purple-to-blue/cyan gradients warn, Inter or Roboto with no display face notes, 3+ identical icon+h3+p cards note, backdrop-filter plus glow notes, gradient text on numerals warns, overshoot cubic-bezier on hover warns. - **Unless:** A purple brand is allowed to be purple; the tell is the constellation and the absence of a decision, not any one colour + +## 38. `color.from-tokens-not-hex` + +Every colour on the page comes from the design system's named tokens or palette functions, never from hex values copied into components. + +- **Source:** GOV.UK Design System (Government Digital Service), *Styles: Colour* — https://design-system.service.gov.uk/styles/colour/ +- **In their words:** "Do not copy the specific hexadecimal (hex) colour values." +- **Do:** Reference colour by role token (brand, text, error, border) rather than by value; Use palette colours (tints and shades of a few families) for supporting elements; Use a functional token only in the context it is designed for +- **Don't:** Hex literals in component styles; Near-duplicate colours (#1d70b8 next to #1d70b9) created by eye-dropping; Using the error token as a general red +- **Look at:** Collect every computed color, background-color, border-*-color, outline-color and fill/stroke of painted elements, and every value of CSS custom properties declared on :root (and on any theme selector). Count distinct painted colours that match no custom-property value (exact RGBA after resolution), and count pairs of painted colours closer than ΔE 2 that are not identical. +- **Unless:** Images, illustrations and embedded third-party widgets; Browser defaults on unstyled native controls; GOV.UK: palette colours (not functional ones) are allowed for illustrations and custom components +- **Also stated as:** color.mode-aware-tokens-in-every-theme (GitHub Primer). + +## 39. `color.controls-and-graphics-3-to-1` + +The parts that show a control is there and what state it is in (input borders, checkbox boxes and ticks, toggle tracks, icon-only buttons, meaningful chart marks) contrast at least 3:1 with the colours next to them. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "Unless the control is inactive, any visual information provided that is necessary for a user to identify that a control is present and how to operate it must have a minimum 3:1 contrast ratio with the adjacent colors." +- **Do:** Input borders at 3:1 against the background the input sits on, or a filled input background at 3:1; Checkbox ticks and radio dots at 3:1 against the box; Standalone icons at 3:1 against their background; Avoid very thin lines that anti-alias below the nominal ratio +- **Don't:** Pale grey input borders (#ddd on white is about 1.4:1); A selected state shown only by a faint tint; Hover effects that lower a control's contrast with its surroundings +- **Look at:** For each visible input, select, textarea, checkbox, radio, [role=switch] and icon-only button (no visible text): compute the contrast ratio between the colour that identifies it (border colour, or its own background when it has no border, or the icon fill) and the background behind it; count those below 3:1, unrounded. Disabled controls are skipped. +- **Unless:** Inactive (disabled) controls are exempt; A control identified by its visible text needs no contrasting boundary; Logos and decorative graphics; Appearance determined by the browser and not modified by the author + +## 40. `color.status-colours-keep-their-meaning` + +Each status colour has one meaning across the product (critical for errors and blocked actions, warning for what needs attention, success for what went well, info for tips) and is never borrowed for promotion or decoration. + +- **Source:** Shopify Polaris, *Colors: Palettes and roles (Critical, Success)* — https://polaris.shopify.com/design/colors/palettes-and-roles +- **In their words:** "Elements using critical must convey messaging that implies that an action is impossible, blocked, or has resulted in an error." +- **Do:** Map error, warning, success and info to named roles or tokens and use them only in those roles; Reserve the critical red for errors, blocked actions and destructive buttons; Use the info role, not warning or critical, for tips and announcements +- **Don't:** A sale or 'new' badge in the error red; Success green used to entice or to advertise an offer; Warning colour for 'coming soon' or 'under construction' messaging; Two different reds meaning error on different screens +- **Look at:** Find the colour the page uses for error text (an element with role=alert, aria-invalid's described-by message, or a class/token named error/critical/danger) and the success colour likewise. Count painted elements (text, fill or border) whose colour equals that error or success colour, within a ΔE of 3, and that are neither a validation message, an invalid field, a status badge of that meaning, nor a destructive action. +- **Unless:** Brand colours that happen to be red, as long as a separate, distinct error red is used for errors; Data visualisations where a series colour coincides with a status hue but no status is implied (prefer avoiding it) + +## 41. `color.one-action-colour-apart-from-status` + +Links and primary actions share one interactive colour family, used consistently, and that colour is not the colour of errors, warnings or success. + +- **Source:** IBM Carbon Design System, *Elements: Color, Overview (Color anatomy)* — https://carbondesignsystem.com/elements/color/overview/ +- **In their words:** "The core blue family serves as the primary action color across all IBM products and experiences. Additional colors are used sparingly and purposefully." +- **Do:** One link colour, used only for links; Primary buttons in the brand or action colour; Danger colour reserved for destructive buttons, not for the primary action +- **Don't:** Links in the error red; Primary buttons in several different hues across screens; Static text coloured like links +- **Look at:** Take the computed text colour of every a[href] in running text and the background of every primary (first, filled, or type=submit) button. Count distinct hues among them (30° bins), and count links or primary buttons whose colour is within ΔE 10 of the page's error, warning or success colour. Also count non-interactive text elements painted in the link colour. +- **Unless:** Primer (GitHub) deliberately uses the success role for primary buttons; a system that states such a mapping consistently is following its own rule; Destructive primary actions (Delete account) take the danger colour on purpose; Navigation menus, where position signals the link + +## 42. `color.focus-ring-contrasts-with-its-surroundings` + +The focus indicator contrasts at least 3:1 with whatever it is drawn against: the page background for an outer ring, the component's own colours for an inner one. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast, Relationship with Focus Visible (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." +- **Do:** An outer ring that contrasts with the page background; A two-colour ring (dark and light) that holds on any background; A thick indicator rather than a 1px one +- **Don't:** A yellow outer ring on a white page; A focus border that changes hue inside the component without contrasting with its fill; Focus shown only by a background tint change +- **Look at:** Tab to each focusable control; diff the focused and unfocused screenshots of the control's box padded by a few pixels; for the changed pixels, take their colour and the colour of the unchanged pixels adjacent to them (page background outside, component fill inside). Count controls where no changed region reaches 3:1 against its adjacent colour. page.focus-visible checks only that some pixel changes; this checks that the change can be seen. +- **Unless:** Unmodified browser default focus styles; WCAG does not compare focused and unfocused states with each other; a background-only change is out of scope for 1.4.11 but fails Use of Color + +## 43. `writing.one-label-per-action` + +One concept, one word: controls that do the same thing carry the same label everywhere, and controls that do different things never share a label. + +- **Source:** GitHub Primer, *Accessibility guide: Descriptive buttons, 'How to test names'* — https://primer.style/guides/accessibility/descriptive-buttons +- **In their words:** "When buttons perform the same action, they have the same name." +- **Do:** Keep a terminology list of preferred words and words not to use for the product; Pick one verb per action (Delete or Remove, not both for the same thing) and reuse it across pages; Add the object to disambiguate repeated actions ('Remove Apples', 'Remove Pears') +- **Don't:** Synonyms for one action across screens: 'Save' here, 'Update' there, 'Apply' elsewhere; Identical labels for different actions on the same page; Naming the same object two ways ('workspace' and 'project') in one product +- **Look at:** Across the journey's pages, collect (accessible name, action) pairs for buttons and links, where action is the form action/href/handler target. Count names that map to two or more different actions on one page, and actions reached by two or more different names across pages; also flag known synonym pairs present together (save/update/apply, delete/remove, sign in/log in, cart/basket). Count must be 0. +- **Unless:** Delete and Remove may coexist when they mean different things (destroy vs take out of a collection), as Carbon defines them — then each must be used only for its own meaning + +## 44. `writing.dates-numbers-units-for-the-reader` + +Dates spell out the month, numbers are numerals with thousands separators, and units sit a space after their number — formatted in the reader's locale, never as an ambiguous all-numeric date or a raw machine value. + +- **Source:** Shopify Polaris, *Content: Grammar and mechanics, 'Numbers, dates, and currency'* — https://polaris.shopify.com/content/grammar-and-mechanics +- **In their words:** "Use the month’s full name. If there isn’t enough space, use 3-letter abbreviations. Don’t write dates with numerals only." +- **Do:** 'December 11, 2024' or 'Dec 11, 2024' (in the reader's locale order); Numerals, not words: 'You have 5 orders to fulfill'; Thousands separators: '12,000'; A space between number and unit: '3.4 lb', '2 kg'; Currency code after the amount when currencies can be confused: '$10,000 USD'; Format with Intl.DateTimeFormat / Intl.NumberFormat for the user's locale +- **Don't:** All-numeric dates like '12/11/24'; ISO timestamps or epoch values shown raw ('2024-12-11T09:30:00Z'); Ordinals in dates ('January 23rd'); Unit glued to the number ('3.4lb'); Shortened numbers like '12 k' where the exact value matters +- **Look at:** Scan visible text nodes. Count matches of all-numeric dates (\b\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}\b), raw ISO timestamps (\d{4}-\d{2}-\d{2}T\d{2}:), integers of 5+ digits with no separator outside codes/IDs, and numbers glued to a unit (\d(kg|lb|cm|mm|km|mi|ml|oz)\b). Count must be 0. +- **Unless:** Polaris notes these are American English base rules and dates, numbers and measurements should be localized automatically — the target is the reader's locale, not US format; Identifiers, codes, SKUs and years are not quantities and take no separator; Dense data tables may use compact numeric dates if the format is unambiguous for the locale and stated in the column header + +## 45. `data-display.no-tables-for-layout` + +A table is for comparing data in rows and columns, never for arranging content on the page; layout belongs to the grid. + +- **Source:** GOV.UK Design System (Government Digital Service), *Table — When not to use this component* — https://design-system.service.gov.uk/components/table/ +- **In their words:** "Never use the table component to layout content on a page." +- **Do:** CSS grid or flex for page and dashboard layout; A list, cards or a summary list for items that do not share columns; Tables only where every row has the same fields +- **Don't:** A table that positions a sidebar, form or dashboard tiles; Table cells holding headings, paragraphs or whole forms; role=presentation on a table that actually holds tabular data +- **Look at:** Count table elements that have no th and either contain headings, more than one paragraph per cell, form fieldsets or nested tables, or have a single row whose cells hold unrelated content blocks. +- **Unless:** HTML email, where tables are still the only reliable layout tool +- **Also stated as:** modern.cards-are-a-choice-not-a-default (Stan Kirilov). + +## 46. `data-display.chart-axes-and-bars-are-labelled` + +Quantitative charts label both axes outside the data area in short plain words, and label each bar with what it shows and its value; past about six bars, use a table. + +- **Source:** Shopify Polaris, *Data visualizations — Axis and labelling conventions; Horizontal bar charts* — https://polaris-react.shopify.com/design/data-visualizations +- **In their words:** "All standard charts that show quantitative data have 2 axes that should be labeled for clarity. … Label each bar with what it’s displaying, as well as the value." +- **Do:** Axis titles naming the measure and its unit; Direct labels on or beside each bar, outside the bar when it is too short; Tick labels skipped at regular intervals rather than crammed or slanted; Short standard abbreviations: 'Feb', '10 Apr', '1.2k'; A legend only when there is more than one series (Primer) +- **Don't:** Unlabelled axes or units only in a tooltip; Slanted tick labels squeezed in to fit; A bar chart of dozens of categories where a table would compare them better +- **Look at:** For each chart, count value or category axes with no visible title or unit text, bars or points with no visible label or value (where there are six or fewer), tick labels rotated away from horizontal, and horizontal bar charts with more than six bars. +- **Unless:** Sparklines and small multiples that share labelled axes with a neighbour; Interactive charts where exact values sit in a tooltip, provided the axes still carry titles and units +- **Also stated as:** canon.tufte-data-ink-ratio (Edward Tufte); canon.tufte-forgo-chartjunk (Edward Tufte). + +## 47. `data-display.chart-axes-are-honest` + +Bar and area charts start their value axis at zero; line charts may crop the axis to show change; gaps in the data are shown as gaps, never interpolated. + +- **Source:** IBM Carbon Design System, *Data visualization — Axes and labels* — https://carbondesignsystem.com/data-visualization/axes-and-labels/ +- **In their words:** "Always start numerical axes at zero for part-to-whole and comparisons charts, such as bar and area chart. … Never interpolate between periods when data is unavailable." +- **Do:** A zero baseline on bar, column and area charts; A cropped y-axis only on line and scatter charts, where the trend is the message; Labelled start and end points around a gap in a time series; An explicit axis-break mark when part of an axis is skipped +- **Don't:** Bars rising from a non-zero baseline; A line drawn straight across missing periods; Changing tick increments to hide missing data +- **Look at:** For each bar, column or area chart (SVG or canvas with an accessible data table or chart library config), count those whose value-axis minimum is not 0 for all-positive data; for each time-series line, count runs where a missing period is bridged by a continuous segment. +- **Unless:** Line charts and scatter plots, where Carbon allows a non-zero start because the trend matters more than relative size; Log-scale charts, which have no zero and must say they are log scale +- **Also stated as:** color.chart-palette-fits-the-data (IBM Carbon Design System); canon.tufte-graphical-integrity (Edward Tufte). + +## 48. `data-display.one-unit-and-format-per-column` + +Every value in a column uses the same unit, precision and format; the unit is named once in the header, not repeated in each cell. + +- **Source:** U.S. Web Design System (GSA), *Table — Usability guidance* — https://designsystem.digital.gov/components/table/ +- **In their words:** "Predictably format columns. Take care not to vary units or formatting within the same column. Instead, normalize values so they can be easily compared." +- **Do:** Normalise a column to one unit (all days, not days and weeks); Keep the same number of decimal places in every row of a column; Put the unit or symbol in the column header ('Temperature °C') rather than in every cell; Use one date format and one thousands separator throughout the table +- **Don't:** Mixing '3 days' and '2 weeks' in one column; '12.5', '12.50' and '12.500' in the same column; Abbreviating some values ('1.2k') and not others in the same column +- **Look at:** For each column of numeric td, count distinct decimal-place counts, distinct unit suffixes or prefixes, and distinct date formats; any column with more than one is a failure. +- **Unless:** A column deliberately mixing magnitudes where the unit changes are labelled per cell and sorting is by underlying value — rare, and worth questioning + +## 49. `data-display.header-cells-are-th-with-scope` + +A data table marks its header cells as th and its data cells as td; when headers run both across and down, scope (or id/headers in complex tables) ties each cell to its headers. + +- **Source:** W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.), *Tables Tutorial* — https://www.w3.org/WAI/tutorials/tables/ +- **In their words:** "Header cells must be marked up with <th>, and data cells with <td> to make tables accessible. For more complex tables, explicit associations may be needed using scope, id, and headers attributes." +- **Do:** A thead row of th scope=col in every data table; th scope=row for the first cell when it names the row's item; id and headers attributes when a cell sits under more than one level of header; A caption that names the table +- **Don't:** Header rows built from td styled bold; Div grids that look like tables with no role=table/row/columnheader/cell semantics; One table holding several topics separated by extra rows of th +- **Look at:** Count table (and role=table/grid) elements that have more than one row of data and no th / role=columnheader; count tables with both a header row and a header column whose th lack scope; count tables with no caption, aria-label or aria-labelledby. +- **Unless:** A simple table with one header row needs only th, not scope; scope matters once there are header rows and header columns; Layout tables, which should not exist (see data-display.no-tables-for-layout) + +## 50. `data-display.long-tables-sort-and-say-so` + +Long tables let users sort the columns that have a natural order, start with one column sorted, and expose the sort state to assistive technology. + +- **Source:** U.S. Web Design System (GSA), *Table — Usability guidance* — https://designsystem.digital.gov/components/table/ +- **In their words:** "Enable sort where useful. Add row sorting to individual columns of long tables where the data can be logically ordered either alphabetically or numerically." +- **Do:** Sort controls as buttons inside the th of sortable columns; One column sorted on load, shown with an arrow and aria-sort on its th (Primer); A polite live region that announces the new sort (USWDS); A filter or search field above long tables, and pagination for very large sets +- **Don't:** Long tables with no sort, filter or pagination; A sort state shown only by an icon with no aria-sort; Row sorting on tables with merged (colspan/rowspan) cells +- **Look at:** Count tables with more than 20 body rows (or with pagination) that have no sort control in any th and no filter/search input associated with them; among sortable tables, count those where no th carries aria-sort other than none. +- **Unless:** Short tables a user takes in at a glance; Tables whose row order is itself the meaning (a ranked list, a timetable) may fix the order + +## 51. `data-display.summary-list-for-key-value-facts` + +A set of facts about one thing — label and value pairs — is shown as a summary list (dl with dt and dd), not as a table with no headers or as loose text. + +- **Source:** GOV.UK Design System (Government Digital Service), *Summary list — When to use / When not to use this component* — https://design-system.service.gov.uk/components/summary-list/ +- **In their words:** "Use a summary list to show information as a list of key facts. … only use it to present information that has a key and at least one value." +- **Do:** dl with dt (the key) and dd (the value) for record details, metadata and check-your-answers pages; A row action ('Change') whose accessible name includes the key ('Change name'); Headings or cards to separate several summary lists on one page +- **Don't:** A two-column table without th used to show one record's fields; Key–value pairs set as 'Label: value' runs in a paragraph; A summary list for genuinely tabular data or a plain list of items +- **Look at:** Where the screen shows the fields of a single record (a profile, an order, an item's metadata, a check-answers page), are the label–value pairs marked up as dl/dt/dd with each key visually distinct from its value — and is tabular data comparing several records in a table rather than a summary list? +- **Unless:** Two or three facts inside a card may be inline text if they are not scanned as a set diff --git a/skills/uxcli/lenses/marketing.json b/skills/uxcli/lenses/marketing.json index 6085166..05e5ece 100644 --- a/skills/uxcli/lenses/marketing.json +++ b/skills/uxcli/lenses/marketing.json @@ -13,24 +13,30 @@ "note": "Same rule, also stated as: modern.signifiers-survive-flatness (Kate Moran (NN/G)); modern.no-dead-zones-and-honest-clickability (Rauno Freiberg)." }, { - "viewpoint": "usability.visual-hierarchy-for-scanning", - "note": "Same rule, also stated as: craft.hierarchy-is-everything-squint-test (Erik D. Kennedy)." + "viewpoint": "usability.omit-needless-words", + "note": "Same rule, also stated as: writing.sentence-case-ui-text (IBM Carbon Design System); writing.plain-language-reading-level (Shopify Polaris)." }, { "viewpoint": "usability.speak-the-users-language" }, { - "viewpoint": "usability.minimalist-no-competing-information" + "viewpoint": "usability.minimalist-no-competing-information", + "note": "Same rule, also stated as: forms.ask-only-what-you-need (GOV.UK Design System)." + }, + { + "viewpoint": "usability.visual-hierarchy-for-scanning", + "note": "Same rule, also stated as: craft.hierarchy-is-everything-squint-test (Erik D. Kennedy)." }, { "viewpoint": "usability.no-false-affordances" }, { - "viewpoint": "usability.follow-conventions", - "note": "Same rule, also stated as: modern.boring-and-familiar-beats-novel (Scott Berkun)." + "viewpoint": "usability.mindless-clicks-not-fewer-clicks", + "note": "Same rule, also stated as: writing.links-describe-their-destination (Mailchimp)." }, { - "viewpoint": "usability.omit-needless-words" + "viewpoint": "usability.follow-conventions", + "note": "Same rule, also stated as: modern.boring-and-familiar-beats-novel (Scott Berkun)." }, { "viewpoint": "usability.banner-blindness-dont-style-content-like-ads" @@ -41,9 +47,6 @@ { "viewpoint": "usability.natural-mapping-and-proximity" }, - { - "viewpoint": "usability.mindless-clicks-not-fewer-clicks" - }, { "viewpoint": "usability.start-with-user-needs-design-with-data" }, @@ -59,6 +62,10 @@ "viewpoint": "craft.de-emphasize-to-emphasize-up-pop-down-pop", "note": "Same rule, also stated as: canon.tufte-smallest-effective-difference (Edward Tufte)." }, + { + "viewpoint": "craft.wcag-contrast-and-dont-rely-on-colour-alone", + "note": "Same rule, also stated as: modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group)." + }, { "viewpoint": "craft.tap-targets-and-control-height", "note": "Same rule, also stated as: modern.hit-targets-meet-platform-minimums (Apple)." @@ -77,10 +84,6 @@ "viewpoint": "craft.text-on-images-needs-consistent-contrast", "note": "Same rule, also stated as: modern.glass-and-blur-earn-their-place (Megan Brown (NN/G))." }, - { - "viewpoint": "craft.wcag-contrast-and-dont-rely-on-colour-alone", - "note": "Same rule, also stated as: modern.contrast-and-not-colour-alone (Apple)." - }, { "viewpoint": "craft.greys-dont-have-to-be-grey-never-use-black" }, @@ -100,12 +103,9 @@ { "viewpoint": "canon.gestalt-closure" }, - { - "viewpoint": "modern.cards-are-a-choice-not-a-default" - }, { "viewpoint": "modern.no-junk-drawer-menus-or-unlabeled-icons", - "note": "Same rule, also stated as: usability.dont-make-me-think (Steve Krug)." + "note": "Same rule, also stated as: writing.buttons-name-the-action (IBM Carbon Design System); usability.dont-make-me-think (Steve Krug)." }, { "viewpoint": "modern.interactions-feel-immediate-under-200ms" @@ -136,6 +136,25 @@ }, { "viewpoint": "modern.defaults-are-decisions-you-inherited" + }, + { + "viewpoint": "color.from-tokens-not-hex" + }, + { + "viewpoint": "color.one-action-colour-apart-from-status" + }, + { + "viewpoint": "color.focus-ring-contrasts-with-its-surroundings" + }, + { + "viewpoint": "color.few-families-in-proportion" + }, + { + "viewpoint": "data-display.no-tables-for-layout", + "note": "Same rule, also stated as: modern.cards-are-a-choice-not-a-default (Stan Kirilov)." + }, + { + "viewpoint": "forms.autocomplete-names-the-purpose" } ] } diff --git a/skills/uxcli/lenses/marketing.md b/skills/uxcli/lenses/marketing.md index ac48fda..7800363 100644 --- a/skills/uxcli/lenses/marketing.md +++ b/skills/uxcli/lenses/marketing.md @@ -2,7 +2,7 @@ Landing, pricing and feature pages: read once, persuade, one call to action. -40 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. +45 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. ## 1. `usability.fitts-target-size-and-distance` @@ -28,17 +28,17 @@ Links and buttons must look clickable — colour, underline, border or fill — - **Unless:** Link position in nav menus or peripheral lists may eliminate the need for underlining (Loranger); Flat works best with low information density, traditional layouts and high-contrast targets positioned standardly (Moran) - **Also stated as:** modern.signifiers-survive-flatness (Kate Moran (NN/G)); modern.no-dead-zones-and-honest-clickability (Rauno Freiberg). -## 3. `usability.visual-hierarchy-for-scanning` +## 3. `usability.omit-needless-words` -Design for scanning, not reading: headings that out-rank body text, bold key phrases, lists, and no walls of unformatted text. +Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. -- **Source:** Kara Pernice, NN/g, *F-Shaped Pattern of Reading on the Web (2017, rev. 2026)* — https://www.nngroup.com/articles/f-shaped-pattern-reading-web-content/ (study) -- **In their words:** "The F-shaped scanning pattern is bad for users and businesses." -- **Do:** Use prominent headings with information-rich opening words; Bold key phrases; employ lists and bullets; Remove unnecessary content -- **Don't:** Leave walls of unstructured text; Let headings fail to out-rank body text visually; Let visual weight contradict importance -- **Look at:** Heading font-size and weight monotonic with level (h1 ≥ h2 ≥ h3 ≥ body); longest paragraph in words; ratio of headings and list items to total text blocks. -- **Unless:** The pattern needs moderate, not high, interest — highly motivated readers read -- **Also stated as:** craft.hierarchy-is-everything-squint-test (Erik D. Kennedy). +- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) +- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" +- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns +- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case +- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. +- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text +- **Also stated as:** writing.sentence-case-ui-text (IBM Carbon Design System); writing.plain-language-reading-level (Shopify Polaris). ## 4. `usability.speak-the-users-language` @@ -61,8 +61,21 @@ Everything on the screen competes with the primary goal; remove what does not se - **Don't:** Let decorative elements distract; Let secondary content outweigh the primary action - **Look at:** At 360 px: count distinct interactive elements above the fold; whether the journey's primary action is within the first viewport; count of elements sharing the accent colour. - **Unless:** Dense expert dashboards where the irreducible information is large +- **Also stated as:** forms.ask-only-what-you-need (GOV.UK Design System). + +## 6. `usability.visual-hierarchy-for-scanning` -## 6. `usability.no-false-affordances` +Design for scanning, not reading: headings that out-rank body text, bold key phrases, lists, and no walls of unformatted text. + +- **Source:** Kara Pernice, NN/g, *F-Shaped Pattern of Reading on the Web (2017, rev. 2026)* — https://www.nngroup.com/articles/f-shaped-pattern-reading-web-content/ (study) +- **In their words:** "The F-shaped scanning pattern is bad for users and businesses." +- **Do:** Use prominent headings with information-rich opening words; Bold key phrases; employ lists and bullets; Remove unnecessary content +- **Don't:** Leave walls of unstructured text; Let headings fail to out-rank body text visually; Let visual weight contradict importance +- **Look at:** Heading font-size and weight monotonic with level (h1 ≥ h2 ≥ h3 ≥ body); longest paragraph in words; ratio of headings and list items to total text blocks. +- **Unless:** The pattern needs moderate, not high, interest — highly motivated readers read +- **Also stated as:** craft.hierarchy-is-everything-squint-test (Erik D. Kennedy). + +## 7. `usability.no-false-affordances` Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. @@ -72,7 +85,19 @@ Nothing that is not interactive may look interactive: no underlined or blue stat - **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons - **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). -## 7. `usability.follow-conventions` +## 8. `usability.mindless-clicks-not-fewer-clicks` + +Do not count clicks; make each one an unambiguous choice with clear link text that says where it goes. + +- **Source:** Steve Krug, *Don't Make Me Think, Revisited — chapter 4 (Krug's Second Law of Usability)* — https://ptgmedia.pearsoncmg.com/images/9780321965516/samplepages/0321965515.pdf +- **In their words:** "It doesn't matter how many times I have to click, as long as each click is a mindless, unambiguous choice." +- **Do:** Write link text that identifies its target; Use breadcrumbs and hub pages for scent; Keep clear labelling with strong information scent +- **Don't:** Count clicks as the metric; Use generic links like 'Click here' or 'Learn more' +- **Look at:** Count links and buttons whose text is generic ('Click here', 'Learn more', 'Read more', bare 'Next') or duplicates another link's text with a different target; do not measure path length. +- **Unless:** Fewer clicks matter more when the same path is drilled repeatedly or pages take long to load (Krug) +- **Also stated as:** writing.links-describe-their-destination (Mailchimp). + +## 9. `usability.follow-conventions` Work the way the sites and platforms users already know; a convention beats a locally optimised novelty. @@ -84,18 +109,7 @@ Work the way the sites and platforms users already know; a convention beats a lo - **Unless:** Desktop apps follow their own OS: Windows OK-first, Apple OK-last - **Also stated as:** modern.boring-and-familiar-beats-novel (Scott Berkun). -## 8. `usability.omit-needless-words` - -Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. - -- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) -- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" -- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns -- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case -- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. -- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text - -## 9. `usability.banner-blindness-dont-style-content-like-ads` +## 10. `usability.banner-blindness-dont-style-content-like-ads` Keep essential content and the primary action in the main column, styled like content, never in a right rail, top strip or animated coloured box. @@ -106,7 +120,7 @@ Keep essential content and the primary action in the main column, styled like co - **Look at:** Is the journey's primary action or a required notice positioned in the right rail (x > 70% of desktop viewport) or in a full-width top strip with background fill and animation? - **Unless:** On mobile, large inline ads do get fixated — the effect is weaker for inline placement -## 10. `usability.aesthetic-usability-effect-bias` +## 11. `usability.aesthetic-usability-effect-bias` Polish makes a screen look more usable than it is; discount your aesthetic impression and run the measurable checks first. @@ -117,7 +131,7 @@ Polish makes a screen look more usable than it is; discount your aesthetic impre - **Look at:** Did the evaluator judge the screen usable because it looks good? Re-check the verdict against the count-kind entries before trusting it; 'attractive' is the variable and stays taste. - **Unless:** With severe usability issues, or functionality sacrificed for aesthetics, users lose patience -## 11. `usability.natural-mapping-and-proximity` +## 12. `usability.natural-mapping-and-proximity` Put a control next to the thing it changes and a label next to its field; gaps between groups must exceed gaps within them. @@ -128,17 +142,6 @@ Put a control next to the thing it changes and a label next to its field; gaps b - **Look at:** Distance from each label to its own input versus the nearest other input; gap within a fieldset versus gap between fieldsets; distance from last input to the submit button. - **Unless:** Left-aligned labels are acceptable if space is constrained and labels are of similar length -## 12. `usability.mindless-clicks-not-fewer-clicks` - -Do not count clicks; make each one an unambiguous choice with clear link text that says where it goes. - -- **Source:** Steve Krug, *Don't Make Me Think, Revisited — chapter 4 (Krug's Second Law of Usability)* — https://ptgmedia.pearsoncmg.com/images/9780321965516/samplepages/0321965515.pdf -- **In their words:** "It doesn't matter how many times I have to click, as long as each click is a mindless, unambiguous choice." -- **Do:** Write link text that identifies its target; Use breadcrumbs and hub pages for scent; Keep clear labelling with strong information scent -- **Don't:** Count clicks as the metric; Use generic links like 'Click here' or 'Learn more' -- **Look at:** Count links and buttons whose text is generic ('Click here', 'Learn more', 'Read more', bare 'Next') or duplicates another link's text with a different target; do not measure path length. -- **Unless:** Fewer clicks matter more when the same path is drilled repeatedly or pages take long to load (Krug) - ## 13. `usability.start-with-user-needs-design-with-data` Name the actor and their need before drawing a screen, then let measured behaviour, not hunches or looks, decide what changes. @@ -185,7 +188,19 @@ To emphasize, quiet the competitors as much as you loudify the hero; never stack - **Unless:** The page title may be all-out up-pop - **Also stated as:** canon.tufte-smallest-effective-difference (Edward Tufte). -## 17. `craft.tap-targets-and-control-height` +## 17. `craft.wcag-contrast-and-dont-rely-on-colour-alone` + +Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. + +- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html +- **In their words:** "The WCAG recommended contrast ratio for body text — 4.5:1 to meet AA standards" +- **Do:** Style coloured badges as a soft background with dark text; Pair every colour state with an icon or label +- **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only +- **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. +- **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself +- **Also stated as:** modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group). + +## 18. `craft.tap-targets-and-control-height` Touch targets are at least 44×44; inputs and the buttons beside them share one height of 40 or 48px. @@ -197,7 +212,7 @@ Touch targets are at least 44×44; inputs and the buttons beside them share one - **Unless:** Inline text links in running prose; Dense desktop-only tools if the project commits to no touch - **Also stated as:** modern.hit-targets-meet-platform-minimums (Apple). -## 18. `craft.start-with-too-much-whitespace` +## 19. `craft.start-with-too-much-whitespace` Start with far more breathing room than feels necessary and remove it until it works; browser-default spacing is the largest mistake. @@ -208,7 +223,7 @@ Start with far more breathing room than feels necessary and remove it until it w - **Look at:** Ratio of vertical padding to font-size on list items, nav items and buttons; ratio of gap between groups to gap within a group. - **Unless:** Dense professional, interaction-heavy tools may compress, but spacing must stay consistent -## 19. `craft.type-scale-few-font-sizes` +## 20. `craft.type-scale-few-font-sizes` Use about four font sizes from a fixed scale; reuse the default size for body, menus, lists and controls. @@ -220,7 +235,7 @@ Use about four font sizes from a fixed scale; reuse the default size for body, m - **Unless:** Sizes must stay distinguishable, so large text may step many points apart; Marketing pages may add a display size - **Also stated as:** canon.bringhurst-compose-with-a-scale (Robert Bringhurst). -## 20. `craft.button-hierarchy-one-primary` +## 21. `craft.button-hierarchy-one-primary` One filled brand-colour button per view; secondaries outlined, tertiaries as text, destructive actions quiet with a confirmation step. @@ -231,7 +246,7 @@ One filled brand-colour button per view; secondaries outlined, tertiaries as tex - **Look at:** Classify buttons as filled, outlined or text from computed style; fail if a view or dialog has more than one filled button of distinct hues, or a delete-labelled button is filled while the confirming action is not. - **Unless:** Segmented or toggle groups; Toolbars of equal-weight actions; A page whose only job is the destructive action, where red is primary -## 21. `craft.text-on-images-needs-consistent-contrast` +## 22. `craft.text-on-images-needs-consistent-contrast` Put an overlay, scrim, box, blur or floor fade between photo and text, and test legibility at every viewport. @@ -243,18 +258,6 @@ Put an overlay, scrim, box, blur or floor fade between photo and text, and test - **Unless:** Decorative text that is not read, such as watermarks; A fixed, dark, low-contrast image - **Also stated as:** modern.glass-and-blur-earn-their-place (Megan Brown (NN/G)). -## 22. `craft.wcag-contrast-and-dont-rely-on-colour-alone` - -Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. - -- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html -- **In their words:** "The WCAG recommended contrast ratio for body text — 4.5:1 to meet AA standards" -- **Do:** Style coloured badges as a soft background with dark text; Pair every colour state with an icon or label -- **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only -- **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. -- **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself -- **Also stated as:** modern.contrast-and-not-colour-alone (Apple). - ## 23. `craft.greys-dont-have-to-be-grey-never-use-black` Tint the neutral scale toward the brand hue and never use pure black for text. @@ -322,18 +325,7 @@ Let something cross the fold or the container edge so the eye knows more continu - **Look at:** At each named viewport: an element crossing the viewport bottom edge; for horizontal scrollers, the last visible item extending past the container edge by a lens-set number of px. - **Unless:** Works less well when viewport sizes are unpredictable; Icons still need labels and testing -## 29. `modern.cards-are-a-choice-not-a-default` - -Cards are for browsing; use lists for searching and tables for comparing, and keep each card readable with one primary action. - -- **Source:** Stan Kirilov, *UI Card Design (StanVision journal)* — https://www.stan.vision/journal/ui-card-design-examples-best-practices-and-common-patterns (secondary) -- **In their words:** "Cards for browsing. Lists for searching. Tables for comparing." -- **Do:** Lists for ranked or homogeneous data, tables for comparison; Consistent card heights; The title as the link, with a pseudo-element extending the hit area -- **Don't:** Card grids for search results and settings; Whole-card anchor wrappers; More than 3 clickable elements per card; Nested cards -- **Look at:** Detect card grids (3+ siblings with equal border, shadow and radius); fail on text under 14px, over 3 interactive descendants per card, row heights differing over 20%, or an anchor wrapping more than heading plus paragraph. -- **Unless:** Masonry where variable height is the design choice; Media browsing such as Netflix or Pinterest - -## 30. `modern.no-junk-drawer-menus-or-unlabeled-icons` +## 29. `modern.no-junk-drawer-menus-or-unlabeled-icons` Icons carry labels, menus are named for what they hold, and no primary feature hides under More or an ellipsis. @@ -343,9 +335,9 @@ Icons carry labels, menus are named for what they hold, and no primary feature h - **Don't:** More, ellipsis or Tools catch-alls holding primary features; Icon-only toolbars without labels; Tooltips on disabled buttons - **Look at:** Icon-only buttons with no text and no aria-label fail; menus triggered by More, …, Tools or Options holding over 5 items or a primary-action label warn; disabled buttons with title or aria-describedby fail. - **Unless:** Universally recognised icons in tight toolbars, though even the hamburger is weaker than designers think -- **Also stated as:** usability.dont-make-me-think (Steve Krug). +- **Also stated as:** writing.buttons-name-the-action (IBM Carbon Design System); usability.dont-make-me-think (Steve Krug). -## 31. `modern.interactions-feel-immediate-under-200ms` +## 30. `modern.interactions-feel-immediate-under-200ms` Interaction transitions run 200ms or less with an ease-out curve so the interface feels immediate. @@ -356,7 +348,7 @@ Interaction transitions run 200ms or less with an ease-out curve so the interfac - **Look at:** Computed transition-duration and animation-duration on elements that change on :hover, :focus, [aria-expanded] or [data-state]; flag any over 300ms, warn over 200ms; ease-in on an entering element fails. - **Unless:** Toasts may run slower with a plain ease on purpose for tone; Large page or scene transitions and decorative loops sit outside the interaction budget; Exit animations can be a bit more relaxed -## 32. `modern.motion-values-proportional-to-trigger` +## 31. `modern.motion-values-proportional-to-trigger` Scale and fade motion starts near its resting size, in proportion to the trigger, never from zero or a heavy squash. @@ -367,7 +359,7 @@ Scale and fade motion starts near its resting size, in proportion to the trigger - **Look at:** Parse @keyframes and WAAPI keyframes on dialogs, popovers and buttons and read the starting scale(); :active transforms below about 0.9 fail. - **Unless:** Elements that genuinely originate from a point, such as a FAB expanding into a sheet, can grow from small -## 33. `modern.animate-only-transform-and-opacity` +## 32. `modern.animate-only-transform-and-opacity` Animate only transform and opacity, listing properties explicitly; never transition all or animate layout properties. @@ -378,7 +370,7 @@ Animate only transform and opacity, listing properties explicitly; never transit - **Look at:** Scan stylesheets and computed transition-property for all and for layout properties; scan @keyframes for width, height, top and left. - **Unless:** Accordion height animation via grid-template-rows or interpolate-size is layout by nature and accepted when it is the only honest way -## 34. `modern.motion-has-an-origin` +## 33. `modern.motion-has-an-origin` A popover or menu animates from the side facing its trigger; set transform-origin where the motion physically starts. @@ -389,7 +381,7 @@ A popover or menu animates from the side facing its trigger; set transform-origi - **Look at:** For each open popover or menu with a known trigger (aria-controls or aria-haspopup), compare the computed transform-origin with the side facing the trigger. - **Unless:** Centred modals have no anchor; a centred origin is right there -## 35. `modern.honour-prefers-reduced-motion` +## 34. `modern.honour-prefers-reduced-motion` Every large motion has a reduced variant under prefers-reduced-motion; fade instead of slide rather than ignoring the setting. @@ -400,7 +392,7 @@ Every large motion has a reduced variant under prefers-reduced-motion; fade inst - **Look at:** Emulate prefers-reduced-motion: reduce and list elements whose computed animation-name or transition-property still includes translations over about 20px or durations above zero. - **Unless:** Motion that is the content, such as a video or a chart drawing, is out of scope -## 36. `modern.mobile-inputs-do-not-zoom-or-trap` +## 35. `modern.mobile-inputs-do-not-zoom-or-trap` Inputs are at least 16px on phones, never autofocus on touch, and the page never disables zoom or blocks paste. @@ -411,7 +403,7 @@ Inputs are at least 16px on phones, never autofocus on touch, and the page never - **Look at:** Computed font-size of input, select and textarea at a 390px viewport under 16px; viewport meta containing user-scalable=no; paste handlers that preventDefault; [autofocus] present at phone width. - **Unless:** Desktop-only admin tools; Autofocus on desktop with a single primary input -## 37. `modern.focus-is-visible-and-unobscured` +## 36. `modern.focus-is-visible-and-unobscured` Every focusable element shows a visible focus ring on :focus-visible, and no sticky or fixed element ever covers it. @@ -421,7 +413,7 @@ Every focusable element shows a visible focus ring on :focus-visible, and no sti - **Don't:** outline: none or 0 with no replacement; Focus rings hidden under sticky headers - **Look at:** Tab through every focusable element; diff the focused and unfocused rect or read computed outline and box-shadow under :focus-visible; check the focused rect is not intersected by position fixed or sticky elements above it. -## 38. `modern.numbers-and-text-do-not-shift-layout` +## 37. `modern.numbers-and-text-do-not-shift-layout` Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. @@ -432,7 +424,7 @@ Numbers in columns and timers use tabular figures, images carry dimensions, and - **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. - **Unless:** Prose numbers -## 39. `modern.shadows-share-one-light-source` +## 38. `modern.shadows-share-one-light-source` All shadows on a page share one light direction and offset ratio, layered and tinted toward the background hue. @@ -443,7 +435,7 @@ All shadows on a page share one light direction and offset ratio, layered and ti - **Look at:** Collect every computed box-shadow; fail when offset signs mix across the page or distinct shadow strings exceed about 6; warn when shadow hue is far from the background hue with alpha above 0.5. - **Unless:** Inset shadows for sunken fields; Glows meant as glows -## 40. `modern.defaults-are-decisions-you-inherited` +## 39. `modern.defaults-are-decisions-you-inherited` A library or AI default is someone else's decision; commit to one written aesthetic direction instead of inheriting it. @@ -453,3 +445,70 @@ A library or AI default is someone else's decision; commit to one written aesthe - **Don't:** Untouched library defaults; Clean and modern as a brief; The AI-look constellation of purple gradient, Inter, identical cards, glass glow and bounce hover - **Look at:** Score the constellation: purple-to-blue/cyan gradients warn, Inter or Roboto with no display face notes, 3+ identical icon+h3+p cards note, backdrop-filter plus glow notes, gradient text on numerals warns, overshoot cubic-bezier on hover warns. - **Unless:** A purple brand is allowed to be purple; the tell is the constellation and the absence of a decision, not any one colour + +## 40. `color.from-tokens-not-hex` + +Every colour on the page comes from the design system's named tokens or palette functions, never from hex values copied into components. + +- **Source:** GOV.UK Design System (Government Digital Service), *Styles: Colour* — https://design-system.service.gov.uk/styles/colour/ +- **In their words:** "Do not copy the specific hexadecimal (hex) colour values." +- **Do:** Reference colour by role token (brand, text, error, border) rather than by value; Use palette colours (tints and shades of a few families) for supporting elements; Use a functional token only in the context it is designed for +- **Don't:** Hex literals in component styles; Near-duplicate colours (#1d70b8 next to #1d70b9) created by eye-dropping; Using the error token as a general red +- **Look at:** Collect every computed color, background-color, border-*-color, outline-color and fill/stroke of painted elements, and every value of CSS custom properties declared on :root (and on any theme selector). Count distinct painted colours that match no custom-property value (exact RGBA after resolution), and count pairs of painted colours closer than ΔE 2 that are not identical. +- **Unless:** Images, illustrations and embedded third-party widgets; Browser defaults on unstyled native controls; GOV.UK: palette colours (not functional ones) are allowed for illustrations and custom components + +## 41. `color.one-action-colour-apart-from-status` + +Links and primary actions share one interactive colour family, used consistently, and that colour is not the colour of errors, warnings or success. + +- **Source:** IBM Carbon Design System, *Elements: Color, Overview (Color anatomy)* — https://carbondesignsystem.com/elements/color/overview/ +- **In their words:** "The core blue family serves as the primary action color across all IBM products and experiences. Additional colors are used sparingly and purposefully." +- **Do:** One link colour, used only for links; Primary buttons in the brand or action colour; Danger colour reserved for destructive buttons, not for the primary action +- **Don't:** Links in the error red; Primary buttons in several different hues across screens; Static text coloured like links +- **Look at:** Take the computed text colour of every a[href] in running text and the background of every primary (first, filled, or type=submit) button. Count distinct hues among them (30° bins), and count links or primary buttons whose colour is within ΔE 10 of the page's error, warning or success colour. Also count non-interactive text elements painted in the link colour. +- **Unless:** Primer (GitHub) deliberately uses the success role for primary buttons; a system that states such a mapping consistently is following its own rule; Destructive primary actions (Delete account) take the danger colour on purpose; Navigation menus, where position signals the link + +## 42. `color.focus-ring-contrasts-with-its-surroundings` + +The focus indicator contrasts at least 3:1 with whatever it is drawn against: the page background for an outer ring, the component's own colours for an inner one. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast, Relationship with Focus Visible (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." +- **Do:** An outer ring that contrasts with the page background; A two-colour ring (dark and light) that holds on any background; A thick indicator rather than a 1px one +- **Don't:** A yellow outer ring on a white page; A focus border that changes hue inside the component without contrasting with its fill; Focus shown only by a background tint change +- **Look at:** Tab to each focusable control; diff the focused and unfocused screenshots of the control's box padded by a few pixels; for the changed pixels, take their colour and the colour of the unchanged pixels adjacent to them (page background outside, component fill inside). Count controls where no changed region reaches 3:1 against its adjacent colour. page.focus-visible checks only that some pixel changes; this checks that the change can be seen. +- **Unless:** Unmodified browser default focus styles; WCAG does not compare focused and unfocused states with each other; a background-only change is out of scope for 1.4.11 but fails Use of Color + +## 43. `color.few-families-in-proportion` + +Use a few colour families in a deliberate proportion (neutral base dominant, then primary, secondary and a small accent), not an even spread of many hues. + +- **Source:** U.S. Web Design System (GSA), *Design tokens: Theme color tokens* — https://designsystem.digital.gov/design-tokens/color/theme-tokens/ +- **In their words:** "about 60% of your site’s color would be the primary color family, about 30% would be the secondary color family, and about 10% would be the accent color families" +- **Do:** Neutral base for text and most surfaces; One primary family carrying most of the colour, one secondary, a small accent; Additional colours used sparingly and for a purpose (Carbon); Start in black and white, then add colour to support the message (USWDS) +- **Don't:** Five or more saturated hue families at similar weight on one screen; An accent that covers more area than the primary; A new hue introduced for a single component +- **Look at:** Screenshot the page and bucket every non-neutral pixel (HSL saturation above about 15%) by hue into 30° bins, ignoring images and status colours. How many hue families take more than 2% of the coloured area, and does the largest one carry most of it while the smallest (accent) stays near a tenth? +- **Unless:** USWDS: the proportions are for non-base colours; neutral text will usually dominate; Illustration, photography and data visualisation, which need their own palettes; Brands whose identity is multi-hue + +## 44. `data-display.no-tables-for-layout` + +A table is for comparing data in rows and columns, never for arranging content on the page; layout belongs to the grid. + +- **Source:** GOV.UK Design System (Government Digital Service), *Table — When not to use this component* — https://design-system.service.gov.uk/components/table/ +- **In their words:** "Never use the table component to layout content on a page." +- **Do:** CSS grid or flex for page and dashboard layout; A list, cards or a summary list for items that do not share columns; Tables only where every row has the same fields +- **Don't:** A table that positions a sidebar, form or dashboard tiles; Table cells holding headings, paragraphs or whole forms; role=presentation on a table that actually holds tabular data +- **Look at:** Count table elements that have no th and either contain headings, more than one paragraph per cell, form fieldsets or nested tables, or have a single row whose cells hold unrelated content blocks. +- **Unless:** HTML email, where tables are still the only reliable layout tool +- **Also stated as:** modern.cards-are-a-choice-not-a-default (Stan Kirilov). + +## 45. `forms.autocomplete-names-the-purpose` + +Every field that asks about the user — name, email, phone, address, postcode, birthday, card — carries the matching autocomplete token so browsers and assistive tech can fill and label it. + +- **Source:** W3C Web Accessibility Initiative, *Understanding WCAG 2.2 Success Criterion 1.3.5: Identify Input Purpose* — https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html +- **In their words:** "Use code to indicate the purpose of common inputs, where technology allows." +- **Do:** autocomplete='name' or 'given-name' and 'family-name' on name fields; 'email', 'tel', 'postal-code', 'street-address' or 'address-line1', 'bday-day'/'bday-month'/'bday-year'; 'shipping' and 'billing' section tokens on order forms; autocomplete='off' on the form, if needed, while each field still declares its purpose +- **Don't:** autocomplete='off' on personal fields with no purpose token; Personal-data fields with no autocomplete attribute; A token that does not match the field (email token on a phone field) +- **Look at:** For every input, select and textarea whose label matches name, email, phone, address, postcode/ZIP, city, country, date of birth, card number or expiry: count those whose autocomplete attribute is missing, 'off', or not a WCAG input-purpose token that matches the label. +- **Unless:** Fields about someone other than the user (a recipient's email) are outside 1.3.5; A field that accepts either username or email may carry one token or none diff --git a/skills/uxcli/lenses/shop.json b/skills/uxcli/lenses/shop.json index eb80d8f..5bf202b 100644 --- a/skills/uxcli/lenses/shop.json +++ b/skills/uxcli/lenses/shop.json @@ -4,9 +4,21 @@ "name": "Shop and catalogue", "when": "Catalogues, search results and product pages: browse, compare, choose.", "picks": [ + { + "viewpoint": "usability.visibility-of-system-status", + "note": "Same rule, also stated as: modern.feedback-is-local-and-optimistic (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn); writing.success-names-what-happened (Shopify Polaris); data-display.loading-skeleton-holds-the-layout (GitHub Primer); forms.do-not-disable-the-submit-button (GitHub Primer)." + }, + { + "viewpoint": "usability.speak-the-users-language", + "note": "Same rule, also stated as: writing.errors-say-what-and-how-to-fix (GOV.UK Design System (Government Digital Service)); writing.plain-language-reading-level (Shopify Polaris); forms.error-message-says-how-to-fix (GOV.UK Design System); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen)." + }, { "viewpoint": "usability.signifiers-make-clickable-look-clickable", - "note": "Same rule, also stated as: modern.signifiers-survive-flatness (Kate Moran (NN/G)); canon.gestalt-similarity (Aurora Harley, NN/g); modern.no-dead-zones-and-honest-clickability (Rauno Freiberg)." + "note": "Same rule, also stated as: usability.banner-blindness-dont-style-content-like-ads (Kara Pernice, NN/g); craft.button-hierarchy-one-primary (Steve Schoger); canon.gestalt-similarity (Aurora Harley, NN/g); color.one-action-colour-apart-from-status (IBM Carbon Design System)." + }, + { + "viewpoint": "usability.feedback-within-a-second", + "note": "Same rule, also stated as: modern.interactions-feel-immediate-under-200ms (Rauno Freiberg)." }, { "viewpoint": "usability.fitts-target-size-and-distance", @@ -14,15 +26,11 @@ }, { "viewpoint": "usability.von-restorff-one-emphasis", - "note": "Same rule, also stated as: craft.button-hierarchy-one-primary (Steve Schoger); canon.vignelli-weight-for-function-not-volume (Massimo Vignelli)." + "note": "Same rule, also stated as: canon.vignelli-weight-for-function-not-volume (Massimo Vignelli)." }, { - "viewpoint": "usability.feedback-within-a-second", - "note": "Same rule, also stated as: modern.interactions-feel-immediate-under-200ms (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn)." - }, - { - "viewpoint": "usability.visibility-of-system-status", - "note": "Same rule, also stated as: modern.feedback-is-local-and-optimistic (Rauno Freiberg)." + "viewpoint": "usability.no-false-affordances", + "note": "Same rule, also stated as: modern.no-dead-zones-and-honest-clickability (Rauno Freiberg)." }, { "viewpoint": "usability.follow-conventions", @@ -36,13 +44,8 @@ "viewpoint": "usability.hicks-fewer-choices-when-time-matters" }, { - "viewpoint": "usability.banner-blindness-dont-style-content-like-ads" - }, - { - "viewpoint": "usability.speak-the-users-language" - }, - { - "viewpoint": "usability.no-false-affordances" + "viewpoint": "usability.mindless-clicks-not-fewer-clicks", + "note": "Same rule, also stated as: writing.links-describe-their-destination (Mailchimp)." }, { "viewpoint": "usability.aesthetic-usability-effect-bias" @@ -53,29 +56,28 @@ { "viewpoint": "usability.natural-mapping-and-proximity" }, - { - "viewpoint": "usability.mindless-clicks-not-fewer-clicks" - }, { "viewpoint": "usability.start-with-user-needs-design-with-data" }, { "viewpoint": "craft.fewer-borders", - "note": "Same rule, also stated as: canon.gestalt-common-region (Aurora Harley, NN/g); canon.rams-as-little-design-as-possible (Dieter Rams); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); canon.tufte-smallest-effective-difference (Edward Tufte); canon.tufte-one-plus-one-equals-three (Edward Tufte)." + "note": "Same rule, also stated as: canon.gestalt-common-region (Aurora Harley, NN/g); forms.group-related-inputs-in-a-fieldset (U.S. Web Design System (USWDS)); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); modern.cards-are-a-choice-not-a-default (Stan Kirilov); canon.tufte-one-plus-one-equals-three (Edward Tufte); craft.separation-order-space-then-lines-then-boxes (Adam Wathan & Steve Schoger; Erik D. Kennedy)." }, { "viewpoint": "craft.spacing-and-sizing-system", - "note": "Same rule, also stated as: canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen); modern.spacing-comes-from-a-scale (Stan Kirilov)." + "note": "Same rule, also stated as: canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.spacing-comes-from-a-scale (Stan Kirilov)." }, { - "viewpoint": "craft.separation-order-space-then-lines-then-boxes" + "viewpoint": "craft.de-emphasize-to-emphasize-up-pop-down-pop", + "note": "Same rule, also stated as: canon.tufte-smallest-effective-difference (Edward Tufte)." }, { - "viewpoint": "craft.de-emphasize-to-emphasize-up-pop-down-pop" + "viewpoint": "craft.wcag-contrast-and-dont-rely-on-colour-alone", + "note": "Same rule, also stated as: modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group)." }, { - "viewpoint": "craft.wcag-contrast-and-dont-rely-on-colour-alone", - "note": "Same rule, also stated as: modern.contrast-and-not-colour-alone (Apple)." + "viewpoint": "craft.black-and-white-first-limit-hues", + "note": "Same rule, also stated as: color.few-families-in-proportion (U.S. Web Design System (GSA))." }, { "viewpoint": "craft.greys-dont-have-to-be-grey-never-use-black" @@ -87,7 +89,7 @@ "viewpoint": "craft.type-scale-few-font-sizes" }, { - "viewpoint": "craft.black-and-white-first-limit-hues" + "viewpoint": "canon.rams-as-little-design-as-possible" }, { "viewpoint": "canon.bringhurst-leading-is-a-rhythmic-unit" @@ -99,15 +101,27 @@ "viewpoint": "canon.bringhurst-letterspace-caps-5-10" }, { - "viewpoint": "modern.cards-are-a-choice-not-a-default" + "viewpoint": "modern.every-state-is-designed", + "note": "Same rule, also stated as: writing.empty-states-say-what-next (GitHub Primer); data-display.empty-data-explains-and-offers-next-step (IBM Carbon Design System); data-display.wrap-before-truncating-and-reveal-the-rest (GitHub Primer)." + }, + { + "viewpoint": "modern.no-junk-drawer-menus-or-unlabeled-icons", + "note": "Same rule, also stated as: writing.buttons-name-the-action (IBM Carbon Design System); usability.dont-make-me-think (Steve Krug)." + }, + { + "viewpoint": "modern.signifiers-survive-flatness" }, { "viewpoint": "modern.frequent-actions-do-not-animate", "note": "Same rule, also stated as: modern.delight-scales-with-rarity (Benji Taylor)." }, { - "viewpoint": "modern.no-junk-drawer-menus-or-unlabeled-icons", - "note": "Same rule, also stated as: usability.dont-make-me-think (Steve Krug)." + "viewpoint": "modern.mobile-inputs-do-not-zoom-or-trap", + "note": "Same rule, also stated as: forms.input-type-matches-the-answer (GOV.UK Design System)." + }, + { + "viewpoint": "modern.numbers-and-text-do-not-shift-layout", + "note": "Same rule, also stated as: data-display.right-align-numbers-tabular-figures (GitHub Primer)." }, { "viewpoint": "modern.motion-values-proportional-to-trigger" @@ -128,22 +142,67 @@ "viewpoint": "modern.readable-type-sizes-and-weights" }, { - "viewpoint": "modern.mobile-inputs-do-not-zoom-or-trap" + "viewpoint": "modern.focus-is-visible-and-unobscured" }, { - "viewpoint": "modern.focus-is-visible-and-unobscured" + "viewpoint": "modern.shadows-share-one-light-source" + }, + { + "viewpoint": "modern.defaults-are-decisions-you-inherited" }, { - "viewpoint": "modern.every-state-is-designed" + "viewpoint": "color.status-colours-keep-their-meaning" }, { - "viewpoint": "modern.numbers-and-text-do-not-shift-layout" + "viewpoint": "color.from-tokens-not-hex" }, { - "viewpoint": "modern.shadows-share-one-light-source" + "viewpoint": "color.controls-and-graphics-3-to-1" }, { - "viewpoint": "modern.defaults-are-decisions-you-inherited" + "viewpoint": "color.focus-ring-contrasts-with-its-surroundings" + }, + { + "viewpoint": "writing.one-label-per-action" + }, + { + "viewpoint": "writing.sentence-case-ui-text" + }, + { + "viewpoint": "writing.dates-numbers-units-for-the-reader" + }, + { + "viewpoint": "data-display.no-tables-for-layout" + }, + { + "viewpoint": "data-display.text-left-headers-follow-their-column" + }, + { + "viewpoint": "data-display.one-unit-and-format-per-column" + }, + { + "viewpoint": "data-display.summary-list-for-key-value-facts" + }, + { + "viewpoint": "forms.error-summary-at-the-top" + }, + { + "viewpoint": "forms.validate-when-the-user-is-done" + }, + { + "viewpoint": "forms.keep-answers-after-an-error" + }, + { + "viewpoint": "forms.mark-optional-fields-in-words" + }, + { + "viewpoint": "forms.autocomplete-names-the-purpose" + }, + { + "viewpoint": "forms.hint-text-is-short-and-linked" + }, + { + "viewpoint": "forms.ask-only-what-you-need" } ] } diff --git a/skills/uxcli/lenses/shop.md b/skills/uxcli/lenses/shop.md index 6453b6e..1f8d753 100644 --- a/skills/uxcli/lenses/shop.md +++ b/skills/uxcli/lenses/shop.md @@ -2,9 +2,33 @@ Catalogues, search results and product pages: browse, compare, choose. -43 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. +60 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. -## 1. `usability.signifiers-make-clickable-look-clickable` +## 1. `usability.visibility-of-system-status` + +Every action with consequences shows the user something changed, as quickly as possible, so they always know what the system is doing. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #1* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "The design should always keep users informed about what is going on, through appropriate feedback within a reasonable amount of time." +- **Do:** Show a visible trace of every state change; Give feedback immediately, or as quickly as possible; Disable and label a control while its action runs +- **Don't:** Submit silently; Change state with no visible trace; Show an action's result only somewhere else +- **Look at:** Drive an action, then diff DOM or pixels over the next frames: did anything visibly change within 1 s; is there a live region, status text, spinner or busy state? +- **Unless:** Below 0.1 s no special feedback is needed beyond showing the result +- **Also stated as:** modern.feedback-is-local-and-optimistic (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn); writing.success-names-what-happened (Shopify Polaris); data-display.loading-skeleton-holds-the-layout (GitHub Primer); forms.do-not-disable-the-submit-button (GitHub Primer). + +## 2. `usability.speak-the-users-language` + +Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #2* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "The design should speak the users' language. Use words, phrases, and concepts familiar to the user, rather than internal jargon." +- **Do:** Use words the user understands without looking them up; Spell acronyms out in full on each page; Write 'for example', not 'eg' or 'ie' +- **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations +- **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. +- **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se +- **Also stated as:** writing.errors-say-what-and-how-to-fix (GOV.UK Design System (Government Digital Service)); writing.plain-language-reading-level (Shopify Polaris); forms.error-message-says-how-to-fix (GOV.UK Design System); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen). + +## 3. `usability.signifiers-make-clickable-look-clickable` Links and buttons must look clickable — colour, underline, border or fill — because weak signifiers cost measured time and fixations. @@ -14,9 +38,21 @@ Links and buttons must look clickable — colour, underline, border or fill — - **Don't:** Style linked text as static text; Use ghost buttons as the default; Use disabled buttons if avoidable; Rely on a label to say that a control is a control - **Look at:** For each a[href], button, [role=button]: computed colour, underline, border and background versus surrounding text; a link matching body colour with no underline and no other differentiator is weak; count disabled buttons. - **Unless:** Link position in nav menus or peripheral lists may eliminate the need for underlining (Loranger); Flat works best with low information density, traditional layouts and high-contrast targets positioned standardly (Moran) -- **Also stated as:** modern.signifiers-survive-flatness (Kate Moran (NN/G)); canon.gestalt-similarity (Aurora Harley, NN/g); modern.no-dead-zones-and-honest-clickability (Rauno Freiberg). +- **Also stated as:** usability.banner-blindness-dont-style-content-like-ads (Kara Pernice, NN/g); craft.button-hierarchy-one-primary (Steve Schoger); canon.gestalt-similarity (Aurora Harley, NN/g); color.one-action-colour-apart-from-status (IBM Carbon Design System). + +## 4. `usability.feedback-within-a-second` + +Paint something within 0.1 s; keep the user's flow with a response under 1 s; show a progress indicator for anything longer. + +- **Source:** Jakob Nielsen, *Response Times: The 3 Important Limits (1993)* — https://www.nngroup.com/articles/response-times-3-important-limits/ (study) +- **In their words:** "0.1 second is about the limit for having the user feel that the system is reacting instantaneously … 1.0 second is about the limit for the user's flow of thought to stay uninterrupted" +- **Do:** Use a progress indicator for any action over about 1 s; Use a looped indicator for 2–9 s waits; Use a percent-done indicator for 10 s or more +- **Don't:** Leave dead time with no indicator; Show a percent-done bar that lies badly +- **Look at:** Time from input event to first paint of any change; presence of a progress element, aria-busy, <progress> or role=status update when the wait exceeds 1 s. +- **Unless:** Laws of UX claims a purposeful delay can raise perceived value — an opinion with no study cited +- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg). -## 2. `usability.fitts-target-size-and-distance` +## 5. `usability.fitts-target-size-and-distance` Make targets big, space them apart, and put them where the pointer already is; touch targets at least 1 cm square. @@ -28,7 +64,7 @@ Make targets big, space them apart, and put them where the pointer already is; t - **Unless:** Infinite targets along screen edges for mouse — size matters less at an edge; Primary CTAs, moving users, children and the elderly need larger than the minimum - **Also stated as:** craft.tap-targets-and-control-height (Erik D. Kennedy); craft.growth-design-psychology-principles (Growth.Design (Dan Benoni, Louis-Xavier Lavallée)); modern.hit-targets-meet-platform-minimums (Apple). -## 3. `usability.von-restorff-one-emphasis` +## 6. `usability.von-restorff-one-emphasis` One visually distinct primary action per view, left-aligned with the form, and distinguished by more than colour. @@ -38,33 +74,20 @@ One visually distinct primary action per view, left-aligned with the form, and d - **Don't:** Put two or more filled primary buttons in one view; Signal emphasis by colour alone - **Look at:** Cluster interactive elements by computed background, border and weight; the most emphatic cluster per view must have one member; primary's left edge aligns with the inputs; emphasis differs in weight, fill or border, not colour only. - **Unless:** Do not pre-highlight a dangerous action as the default (Nielsen) -- **Also stated as:** craft.button-hierarchy-one-primary (Steve Schoger); canon.vignelli-weight-for-function-not-volume (Massimo Vignelli). - -## 4. `usability.feedback-within-a-second` +- **Also stated as:** canon.vignelli-weight-for-function-not-volume (Massimo Vignelli). -Paint something within 0.1 s; keep the user's flow with a response under 1 s; show a progress indicator for anything longer. +## 7. `usability.no-false-affordances` -- **Source:** Jakob Nielsen, *Response Times: The 3 Important Limits (1993)* — https://www.nngroup.com/articles/response-times-3-important-limits/ (study) -- **In their words:** "0.1 second is about the limit for having the user feel that the system is reacting instantaneously … 1.0 second is about the limit for the user's flow of thought to stay uninterrupted" -- **Do:** Use a progress indicator for any action over about 1 s; Use a looped indicator for 2–9 s waits; Use a percent-done indicator for 10 s or more -- **Don't:** Leave dead time with no indicator; Show a percent-done bar that lies badly -- **Look at:** Time from input event to first paint of any change; presence of a progress element, aria-busy, <progress> or role=status update when the wait exceeds 1 s. -- **Unless:** Laws of UX claims a purposeful delay can raise perceived value — an opinion with no study cited -- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn). - -## 5. `usability.visibility-of-system-status` - -Every action with consequences shows the user something changed, as quickly as possible, so they always know what the system is doing. +Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #1* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "The design should always keep users informed about what is going on, through appropriate feedback within a reasonable amount of time." -- **Do:** Show a visible trace of every state change; Give feedback immediately, or as quickly as possible; Disable and label a control while its action runs -- **Don't:** Submit silently; Change state with no visible trace; Show an action's result only somewhere else -- **Look at:** Drive an action, then diff DOM or pixels over the next frames: did anything visibly change within 1 s; is there a live region, status text, spinner or busy state? -- **Unless:** Below 0.1 s no special feedback is needed beyond showing the result -- **Also stated as:** modern.feedback-is-local-and-optimistic (Rauno Freiberg). +- **Source:** Hoa Loranger, NN/g, *Beyond Blue Links: Making Clickable Elements Recognizable (2015)* — https://www.nngroup.com/articles/clickable-elements/ +- **In their words:** "Avoid making non-clickable items (like headings) resemble buttons." +- **Do:** Reserve link colour and underline for links; Reserve button shape and fill for buttons +- **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons +- **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). +- **Also stated as:** modern.no-dead-zones-and-honest-clickability (Rauno Freiberg). -## 6. `usability.follow-conventions` +## 8. `usability.follow-conventions` Work the way the sites and platforms users already know; a convention beats a locally optimised novelty. @@ -76,7 +99,7 @@ Work the way the sites and platforms users already know; a convention beats a lo - **Unless:** Desktop apps follow their own OS: Windows OK-first, Apple OK-last - **Also stated as:** craft.anything-but-dropdowns (Erik D. Kennedy); modern.boring-and-familiar-beats-novel (Scott Berkun). -## 7. `usability.visual-hierarchy-for-scanning` +## 9. `usability.visual-hierarchy-for-scanning` Design for scanning, not reading: headings that out-rank body text, bold key phrases, lists, and no walls of unformatted text. @@ -88,7 +111,7 @@ Design for scanning, not reading: headings that out-rank body text, bold key phr - **Unless:** The pattern needs moderate, not high, interest — highly motivated readers read - **Also stated as:** craft.hierarchy-is-everything-squint-test (Erik D. Kennedy). -## 8. `usability.hicks-fewer-choices-when-time-matters` +## 10. `usability.hicks-fewer-choices-when-time-matters` Fewer equal-weight choices at a decision point, with one recommended; but never use seven as a cap on menu length. @@ -99,37 +122,17 @@ Fewer equal-weight choices at a decision point, with one recommended; but never - **Look at:** Count same-weight primary actions in a view; count options in a select or radio group with no recommended default. A menu's item count alone is not a failure. - **Unless:** Recognition, not recall, governs menus — length is a scan cost, not a memory cost (Nielsen) -## 9. `usability.banner-blindness-dont-style-content-like-ads` - -Keep essential content and the primary action in the main column, styled like content, never in a right rail, top strip or animated coloured box. - -- **Source:** Kara Pernice, NN/g, *Banner Blindness Revisited (2018)* — https://www.nngroup.com/articles/banner-blindness-old-and-new-findings/ (study) -- **In their words:** "Users have learned to ignore content that resembles ads, is close to ads, or appears in locations traditionally dedicated to ads." -- **Do:** Put essential content in the main column; Style key notices like content -- **Don't:** Put the primary CTA in the right rail or a top banner strip; Give a key notice animation, coloured background or fancy formatting; Place essential content next to real ads -- **Look at:** Is the journey's primary action or a required notice positioned in the right rail (x > 70% of desktop viewport) or in a full-width top strip with background fill and animation? -- **Unless:** On mobile, large inline ads do get fixated — the effect is weaker for inline placement - -## 10. `usability.speak-the-users-language` - -Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. +## 11. `usability.mindless-clicks-not-fewer-clicks` -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #2* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "The design should speak the users' language. Use words, phrases, and concepts familiar to the user, rather than internal jargon." -- **Do:** Use words the user understands without looking them up; Spell acronyms out in full on each page; Write 'for example', not 'eg' or 'ie' -- **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations -- **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. -- **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se - -## 11. `usability.no-false-affordances` - -Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. +Do not count clicks; make each one an unambiguous choice with clear link text that says where it goes. -- **Source:** Hoa Loranger, NN/g, *Beyond Blue Links: Making Clickable Elements Recognizable (2015)* — https://www.nngroup.com/articles/clickable-elements/ -- **In their words:** "Avoid making non-clickable items (like headings) resemble buttons." -- **Do:** Reserve link colour and underline for links; Reserve button shape and fill for buttons -- **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons -- **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). +- **Source:** Steve Krug, *Don't Make Me Think, Revisited — chapter 4 (Krug's Second Law of Usability)* — https://ptgmedia.pearsoncmg.com/images/9780321965516/samplepages/0321965515.pdf +- **In their words:** "It doesn't matter how many times I have to click, as long as each click is a mindless, unambiguous choice." +- **Do:** Write link text that identifies its target; Use breadcrumbs and hub pages for scent; Keep clear labelling with strong information scent +- **Don't:** Count clicks as the metric; Use generic links like 'Click here' or 'Learn more' +- **Look at:** Count links and buttons whose text is generic ('Click here', 'Learn more', 'Read more', bare 'Next') or duplicates another link's text with a different target; do not measure path length. +- **Unless:** Fewer clicks matter more when the same path is drilled repeatedly or pages take long to load (Krug) +- **Also stated as:** writing.links-describe-their-destination (Mailchimp). ## 12. `usability.aesthetic-usability-effect-bias` @@ -164,18 +167,7 @@ Put a control next to the thing it changes and a label next to its field; gaps b - **Look at:** Distance from each label to its own input versus the nearest other input; gap within a fieldset versus gap between fieldsets; distance from last input to the submit button. - **Unless:** Left-aligned labels are acceptable if space is constrained and labels are of similar length -## 15. `usability.mindless-clicks-not-fewer-clicks` - -Do not count clicks; make each one an unambiguous choice with clear link text that says where it goes. - -- **Source:** Steve Krug, *Don't Make Me Think, Revisited — chapter 4 (Krug's Second Law of Usability)* — https://ptgmedia.pearsoncmg.com/images/9780321965516/samplepages/0321965515.pdf -- **In their words:** "It doesn't matter how many times I have to click, as long as each click is a mindless, unambiguous choice." -- **Do:** Write link text that identifies its target; Use breadcrumbs and hub pages for scent; Keep clear labelling with strong information scent -- **Don't:** Count clicks as the metric; Use generic links like 'Click here' or 'Learn more' -- **Look at:** Count links and buttons whose text is generic ('Click here', 'Learn more', 'Read more', bare 'Next') or duplicates another link's text with a different target; do not measure path length. -- **Unless:** Fewer clicks matter more when the same path is drilled repeatedly or pages take long to load (Krug) - -## 16. `usability.start-with-user-needs-design-with-data` +## 15. `usability.start-with-user-needs-design-with-data` Name the actor and their need before drawing a screen, then let measured behaviour, not hunches or looks, decide what changes. @@ -185,7 +177,7 @@ Name the actor and their need before drawing a screen, then let measured behavio - **Don't:** Design to a hunch; Design to an aesthetic - **Look at:** Does the journey name its actor and need (uxcli context show), and is the design decision traceable to observed behaviour rather than a hunch or a look? -## 17. `craft.fewer-borders` +## 16. `craft.fewer-borders` Separate elements with space, a background shift, a shadow or striping before reaching for a border; too many borders make a design busy. @@ -195,9 +187,9 @@ Separate elements with space, a background shift, a shadow or striping before re - **Don't:** Use a 1px border as the default grouping device; Wrap a nav in a panel that only needs to recede; Rule every table row with a border when striping would do - **Look at:** For every text node, count ancestors with a visible border, non-none box-shadow or outline; report max nesting depth and bordered boxes per viewport. Counted by `page.nesting`: `review check` refuses a `holds` it contradicts. - **Unless:** Form inputs and the one object a person acts on keep their border; Keylines that make disconnected content feel connected; Decorative borders as style once the count is low -- **Also stated as:** canon.gestalt-common-region (Aurora Harley, NN/g); canon.rams-as-little-design-as-possible (Dieter Rams); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); canon.tufte-smallest-effective-difference (Edward Tufte); canon.tufte-one-plus-one-equals-three (Edward Tufte). +- **Also stated as:** canon.gestalt-common-region (Aurora Harley, NN/g); forms.group-related-inputs-in-a-fieldset (U.S. Web Design System (USWDS)); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); modern.cards-are-a-choice-not-a-default (Stan Kirilov); canon.tufte-one-plus-one-equals-three (Edward Tufte); craft.separation-order-space-then-lines-then-boxes (Adam Wathan & Steve Schoger; Erik D. Kennedy). -## 18. `craft.spacing-and-sizing-system` +## 17. `craft.spacing-and-sizing-system` Draw every margin, padding and gap from one scale, and make space between groups clearly larger than space within them. @@ -207,19 +199,9 @@ Draw every margin, padding and gap from one scale, and make space between groups - **Don't:** Use one-off values such as 13px or 27px; Use equal gaps across a group boundary - **Look at:** Collect all computed margin, padding and gap values above 0; report distinct values and how many are off a 4px grid; compare label-to-field gap with field-to-next-label gap. - **Unless:** Optical adjustments on icons and hanging punctuation sit a pixel or two off-scale -- **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen); modern.spacing-comes-from-a-scale (Stan Kirilov). - -## 19. `craft.separation-order-space-then-lines-then-boxes` - -Use the lightest separator that works: more space first, then a keyline or background band, and a box only for the object that is acted on. - -- **Source:** Adam Wathan & Steve Schoger; Erik D. Kennedy, *Refactoring UI (fewer-borders tactic); 7 Rules for Creating Gorgeous UI, Part 1* — https://www.refactoringui.com/ (folklore: the wording is not verified) -- **Do:** Separate with whitespace by default; Add a keyline or background band only when space alone fails; Box only the object a person acts on -- **Don't:** Reach for a card or border first -- **Look at:** Between sibling groups, record which separator is used: gap at least 2x the inner gap (space), hr or border-bottom (line), bordered or shadowed wrapper (box); report the inner/outer gap ratio. -- **Unless:** Dense data such as tables where zebra stripes or keylines are used; Interactive cards that are the unit of action +- **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.spacing-comes-from-a-scale (Stan Kirilov). -## 20. `craft.de-emphasize-to-emphasize-up-pop-down-pop` +## 18. `craft.de-emphasize-to-emphasize-up-pop-down-pop` To emphasize, quiet the competitors as much as you loudify the hero; never stack every up-pop property on non-title elements. @@ -229,8 +211,9 @@ To emphasize, quiet the competitors as much as you loudify the hero; never stack - **Don't:** Stack big, bold, bright and uppercase on any element that is not the page title - **Look at:** Flag text at 2x body size or more that is also font-weight 700+, full contrast and uppercase, when it is not the page h1. - **Unless:** The page title may be all-out up-pop +- **Also stated as:** canon.tufte-smallest-effective-difference (Edward Tufte). -## 21. `craft.wcag-contrast-and-dont-rely-on-colour-alone` +## 19. `craft.wcag-contrast-and-dont-rely-on-colour-alone` Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. @@ -240,9 +223,21 @@ Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dar - **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only - **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. - **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself -- **Also stated as:** modern.contrast-and-not-colour-alone (Apple). +- **Also stated as:** modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group). -## 22. `craft.greys-dont-have-to-be-grey-never-use-black` +## 20. `craft.black-and-white-first-limit-hues` + +Design in greyscale first, then add one accent hue with purpose; vary by saturation and brightness, not by more hues. + +- **Source:** Erik D. Kennedy, *7 Rules for Creating Gorgeous UI, Part 1* — https://www.learnui.design/blog/7-rules-for-creating-gorgeous-ui-part-1.html +- **In their words:** "Design black and white first … Add color last, and even then, only with purpose." +- **Do:** Use greyscale plus one or two accent hues; Vary colour by saturation and brightness within a hue +- **Don't:** Use complementary or triadic colour-theory palettes as UI palettes; Give each section its own hue +- **Look at:** Cluster computed colours with saturation above 25% by hue within 15 degrees; count clusters, excluding declared semantic states such as error, success and warning. +- **Unless:** Sporty, flashy or cartoony brands that need a colour-fluent designer; Data-visualisation palettes +- **Also stated as:** color.few-families-in-proportion (U.S. Web Design System (GSA)). + +## 21. `craft.greys-dont-have-to-be-grey-never-use-black` Tint the neutral scale toward the brand hue and never use pure black for text. @@ -253,7 +248,7 @@ Tint the neutral scale toward the brand hue and never use pure black for text. - **Look at:** Count text, border and background colours with saturation 0 and lightness under 15%, and the share of neutral swatches with saturation exactly 0. - **Unless:** It does not always work; worth a trial; High-contrast or accessibility modes and print -## 23. `craft.start-with-too-much-whitespace` +## 22. `craft.start-with-too-much-whitespace` Start with far more breathing room than feels necessary and remove it until it works; browser-default spacing is the largest mistake. @@ -264,7 +259,7 @@ Start with far more breathing room than feels necessary and remove it until it w - **Look at:** Ratio of vertical padding to font-size on list items, nav items and buttons; ratio of gap between groups to gap within a group. - **Unless:** Dense professional, interaction-heavy tools may compress, but spacing must stay consistent -## 24. `craft.type-scale-few-font-sizes` +## 23. `craft.type-scale-few-font-sizes` Use about four font sizes from a fixed scale; reuse the default size for body, menus, lists and controls. @@ -275,18 +270,18 @@ Use about four font sizes from a fixed scale; reuse the default size for body, m - **Look at:** Count distinct computed font-size values on visible text per viewport and how many fall outside the declared scale. - **Unless:** Sizes must stay distinguishable, so large text may step many points apart; Marketing pages may add a display size -## 25. `craft.black-and-white-first-limit-hues` +## 24. `canon.rams-as-little-design-as-possible` -Design in greyscale first, then add one accent hue with purpose; vary by saturation and brightness, not by more hues. +Remove everything that does not serve the purpose; less, but better. -- **Source:** Erik D. Kennedy, *7 Rules for Creating Gorgeous UI, Part 1* — https://www.learnui.design/blog/7-rules-for-creating-gorgeous-ui-part-1.html -- **In their words:** "Design black and white first … Add color last, and even then, only with purpose." -- **Do:** Use greyscale plus one or two accent hues; Vary colour by saturation and brightness within a hue -- **Don't:** Use complementary or triadic colour-theory palettes as UI palettes; Give each section its own hue -- **Look at:** Cluster computed colours with saturation above 25% by hue within 15 degrees; count clusters, excluding declared semantic states such as error, success and warning. -- **Unless:** Sporty, flashy or cartoony brands that need a colour-fluent designer; Data-visualisation palettes +- **Source:** Dieter Rams, *Ten principles for good design* — https://www.vitsoe.com/us/about/good-design +- **In their words:** "Good design is as little design as possible. Less, but better – because it concentrates on the essential aspects, and the products are not burdened with non-essentials." +- **Do:** Remove anything that does not serve the purpose; Keep one border where one will do +- **Don't:** Add ornament or decoration for its own sake; Repeat the heading as a decorative icon; Wrap content in extra bordered or shadowed layers +- **Look at:** Decorative-only elements (no text, image or interactive role, only border/shadow/background) and distinct box-shadow, border-radius and gradient declarations in computed styles. +- **Unless:** Aesthetic quality is part of usefulness (principle 3): stripping to nothing is not the goal -## 26. `canon.bringhurst-leading-is-a-rhythmic-unit` +## 25. `canon.bringhurst-leading-is-a-rhythmic-unit` The leading is the vertical unit; add and remove vertical space in multiples of it. @@ -297,7 +292,7 @@ The leading is the vertical unit; add and remove vertical space in multiples of - **Look at:** Body line-height L; margins and paddings between text blocks as multiples of L (or L/2 if the lens allows). - **Unless:** More leading for longer measures, darker faces, larger x-height and sans serifs: the ratio moves with the face -## 27. `canon.vignelli-grid-module-fits-the-job` +## 26. `canon.vignelli-grid-module-fits-the-job` Lay everything on one grid whose module is coarse enough to constrain and fine enough to serve the content. @@ -308,7 +303,7 @@ Lay everything on one grid whose module is coarse enough to constrain and fine e - **Look at:** Distinct left-edge x positions of top-level blocks and their fit to the best k-column model (residual within 2px). - **Unless:** Outside margins may be made deliberately small to give tension between page edge and content -## 28. `canon.bringhurst-letterspace-caps-5-10` +## 27. `canon.bringhurst-letterspace-caps-5-10` Letterspace all-caps and small-caps runs by 5–10% of the type size; do not letterspace lowercase. @@ -319,18 +314,41 @@ Letterspace all-caps and small-caps runs by 5–10% of the type size; do not let - **Look at:** For text with text-transform uppercase or all-caps content of ≥3 letters: computed letter-spacing divided by font-size; same ratio on lowercase paragraphs. - **Unless:** Display type at large sizes may be tracked tighter (not stated on the fetched page) -## 29. `modern.cards-are-a-choice-not-a-default` +## 28. `modern.every-state-is-designed` + +Empty, sparse, dense, error and loading states are designed; skeletons match final layout and long content never overflows. -Cards are for browsing; use lists for searching and tables for comparing, and keep each card readable with one primary action. +- **Source:** Vercel Labs, *Web Interface Guidelines — AGENTS.md* — https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/AGENTS.md +- **In their words:** "MUST: Design empty/sparse/dense/error states" +- **Do:** An empty state with a primary create action; Skeletons sized like real rows; min-w-0 and truncation on flex children +- **Don't:** Blank screens on empty arrays; Spinners that reflow content; Overflow from long strings +- **Look at:** Render with [], with 1 item, with 500 items and with a 300-character title; assert no horizontal overflow, no overlap, an actionable control in the empty state, and skeleton-to-loaded CLS under a threshold. +- **Also stated as:** writing.empty-states-say-what-next (GitHub Primer); data-display.empty-data-explains-and-offers-next-step (IBM Carbon Design System); data-display.wrap-before-truncating-and-reveal-the-rest (GitHub Primer). -- **Source:** Stan Kirilov, *UI Card Design (StanVision journal)* — https://www.stan.vision/journal/ui-card-design-examples-best-practices-and-common-patterns (secondary) -- **In their words:** "Cards for browsing. Lists for searching. Tables for comparing." -- **Do:** Lists for ranked or homogeneous data, tables for comparison; Consistent card heights; The title as the link, with a pseudo-element extending the hit area -- **Don't:** Card grids for search results and settings; Whole-card anchor wrappers; More than 3 clickable elements per card; Nested cards -- **Look at:** Detect card grids (3+ siblings with equal border, shadow and radius); fail on text under 14px, over 3 interactive descendants per card, row heights differing over 20%, or an anchor wrapping more than heading plus paragraph. -- **Unless:** Masonry where variable height is the design choice; Media browsing such as Netflix or Pinterest +## 29. `modern.no-junk-drawer-menus-or-unlabeled-icons` -## 30. `modern.frequent-actions-do-not-animate` +Icons carry labels, menus are named for what they hold, and no primary feature hides under More or an ellipsis. + +- **Source:** Jakob Nielsen (NN/G), *Top 10 Application-Design Mistakes* — https://www.nngroup.com/articles/top-10-application-design-mistakes/ +- **In their words:** "most icons, unless they have a text label next to them, will be difficult or impossible for users to understand" +- **Do:** Labelled icons; Menus named for their contents; Inline help before tooltips +- **Don't:** More, ellipsis or Tools catch-alls holding primary features; Icon-only toolbars without labels; Tooltips on disabled buttons +- **Look at:** Icon-only buttons with no text and no aria-label fail; menus triggered by More, …, Tools or Options holding over 5 items or a primary-action label warn; disabled buttons with title or aria-describedby fail. +- **Unless:** Universally recognised icons in tight toolbars, though even the hamburger is weaker than designers think +- **Also stated as:** writing.buttons-name-the-action (IBM Carbon Design System); usability.dont-make-me-think (Steve Krug). + +## 30. `modern.signifiers-survive-flatness` + +Flat styling keeps its signifiers: buttons have a fill or border, links are underlined or distinctly coloured, inputs show a boundary. + +- **Source:** Kate Moran (NN/G), *Flat UI Elements Attract Less Attention and Cause Uncertainty* — https://www.nngroup.com/articles/flat-ui-less-attention-cause-uncertainty/ (study) +- **In their words:** "participants spent 22% more time… looking at the pages with weak signifiers" +- **Do:** Buttons with a discernible fill or border; Links underlined or in a distinct colour; Inputs with a visible boundary +- **Don't:** Text-only buttons indistinguishable from labels; Links styled as body text; Long shadows and other purely aesthetic depth +- **Look at:** For each button or role=button: background differs from parent, or border width above 0, or text colour is the accent, else fail; an in-text link with no text-decoration and colour close to the surrounding text fails. +- **Unless:** Toolbar icon buttons with tooltips and hover fills; Navigation bars recognised by position + +## 31. `modern.frequent-actions-do-not-animate` Actions used many times a day, and anything keyboard-triggered, appear instantly without an enter animation. @@ -342,19 +360,31 @@ Actions used many times a day, and anything keyboard-triggered, appear instantly - **Unless:** macOS context menus fade out and blink the chosen item; Rare features may be theatrical (delight-impact curve) - **Also stated as:** modern.delight-scales-with-rarity (Benji Taylor). -## 31. `modern.no-junk-drawer-menus-or-unlabeled-icons` +## 32. `modern.mobile-inputs-do-not-zoom-or-trap` -Icons carry labels, menus are named for what they hold, and no primary feature hides under More or an ellipsis. +Inputs are at least 16px on phones, never autofocus on touch, and the page never disables zoom or blocks paste. -- **Source:** Jakob Nielsen (NN/G), *Top 10 Application-Design Mistakes* — https://www.nngroup.com/articles/top-10-application-design-mistakes/ -- **In their words:** "most icons, unless they have a text label next to them, will be difficult or impossible for users to understand" -- **Do:** Labelled icons; Menus named for their contents; Inline help before tooltips -- **Don't:** More, ellipsis or Tools catch-alls holding primary features; Icon-only toolbars without labels; Tooltips on disabled buttons -- **Look at:** Icon-only buttons with no text and no aria-label fail; menus triggered by More, …, Tools or Options holding over 5 items or a primary-action label warn; disabled buttons with title or aria-describedby fail. -- **Unless:** Universally recognised icons in tight toolbars, though even the hamburger is weaker than designers think -- **Also stated as:** usability.dont-make-me-think (Steve Krug). +- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ +- **In their words:** "Font size for inputs should not be smaller than 16px to prevent iOS zooming on focus" +- **Do:** Set type, inputmode and autocomplete on every input; Use touch-action: manipulation on controls +- **Don't:** Viewport meta with user-scalable=no; Paste handlers that preventDefault; autofocus at phone widths +- **Look at:** Computed font-size of input, select and textarea at a 390px viewport under 16px; viewport meta containing user-scalable=no; paste handlers that preventDefault; [autofocus] present at phone width. +- **Unless:** Desktop-only admin tools; Autofocus on desktop with a single primary input +- **Also stated as:** forms.input-type-matches-the-answer (GOV.UK Design System). -## 32. `modern.motion-values-proportional-to-trigger` +## 33. `modern.numbers-and-text-do-not-shift-layout` + +Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. + +- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ +- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" +- **Do:** tabular-nums in tables, timers and prices; width and height on every img +- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover +- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. +- **Unless:** Prose numbers +- **Also stated as:** data-display.right-align-numbers-tabular-figures (GitHub Primer). + +## 34. `modern.motion-values-proportional-to-trigger` Scale and fade motion starts near its resting size, in proportion to the trigger, never from zero or a heavy squash. @@ -365,7 +395,7 @@ Scale and fade motion starts near its resting size, in proportion to the trigger - **Look at:** Parse @keyframes and WAAPI keyframes on dialogs, popovers and buttons and read the starting scale(); :active transforms below about 0.9 fail. - **Unless:** Elements that genuinely originate from a point, such as a FAB expanding into a sheet, can grow from small -## 33. `modern.animate-only-transform-and-opacity` +## 35. `modern.animate-only-transform-and-opacity` Animate only transform and opacity, listing properties explicitly; never transition all or animate layout properties. @@ -376,7 +406,7 @@ Animate only transform and opacity, listing properties explicitly; never transit - **Look at:** Scan stylesheets and computed transition-property for all and for layout properties; scan @keyframes for width, height, top and left. - **Unless:** Accordion height animation via grid-template-rows or interpolate-size is layout by nature and accepted when it is the only honest way -## 34. `modern.motion-has-an-origin` +## 36. `modern.motion-has-an-origin` A popover or menu animates from the side facing its trigger; set transform-origin where the motion physically starts. @@ -387,7 +417,7 @@ A popover or menu animates from the side facing its trigger; set transform-origi - **Look at:** For each open popover or menu with a known trigger (aria-controls or aria-haspopup), compare the computed transform-origin with the side facing the trigger. - **Unless:** Centred modals have no anchor; a centred origin is right there -## 35. `modern.honour-prefers-reduced-motion` +## 37. `modern.honour-prefers-reduced-motion` Every large motion has a reduced variant under prefers-reduced-motion; fade instead of slide rather than ignoring the setting. @@ -398,7 +428,7 @@ Every large motion has a reduced variant under prefers-reduced-motion; fade inst - **Look at:** Emulate prefers-reduced-motion: reduce and list elements whose computed animation-name or transition-property still includes translations over about 20px or durations above zero. - **Unless:** Motion that is the content, such as a video or a chart drawing, is out of scope -## 36. `modern.animations-are-interruptible` +## 38. `modern.animations-are-interruptible` An open or close animation can be reversed mid-flight by the next input; nothing waits for a transition to finish. @@ -409,7 +439,7 @@ An open or close animation can be reversed mid-flight by the next input; nothing - **Look at:** Open a panel, immediately send the close input, sample the element's bounding box about 50ms later; if it is still growing the animation was not interruptible. Depends on timing tolerance. - **Unless:** Destructive commits that fire only on gesture end are about triggering, not interruptibility -## 37. `modern.readable-type-sizes-and-weights` +## 39. `modern.readable-type-sizes-and-weights` Body text sits at the platform default size, weights stay 400 or heavier, and weight never changes on hover. @@ -420,18 +450,7 @@ Body text sits at the platform default size, weights stay 400 or heavier, and we - **Look at:** Computed font-size and font-weight of every text node at a phone viewport; count distinct font-family stacks and warn above 2. - **Unless:** Captions and legal text may sit at the platform minimum; Display headings may use light weights at large sizes -## 38. `modern.mobile-inputs-do-not-zoom-or-trap` - -Inputs are at least 16px on phones, never autofocus on touch, and the page never disables zoom or blocks paste. - -- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ -- **In their words:** "Font size for inputs should not be smaller than 16px to prevent iOS zooming on focus" -- **Do:** Set type, inputmode and autocomplete on every input; Use touch-action: manipulation on controls -- **Don't:** Viewport meta with user-scalable=no; Paste handlers that preventDefault; autofocus at phone widths -- **Look at:** Computed font-size of input, select and textarea at a 390px viewport under 16px; viewport meta containing user-scalable=no; paste handlers that preventDefault; [autofocus] present at phone width. -- **Unless:** Desktop-only admin tools; Autofocus on desktop with a single primary input - -## 39. `modern.focus-is-visible-and-unobscured` +## 40. `modern.focus-is-visible-and-unobscured` Every focusable element shows a visible focus ring on :focus-visible, and no sticky or fixed element ever covers it. @@ -441,28 +460,7 @@ Every focusable element shows a visible focus ring on :focus-visible, and no sti - **Don't:** outline: none or 0 with no replacement; Focus rings hidden under sticky headers - **Look at:** Tab through every focusable element; diff the focused and unfocused rect or read computed outline and box-shadow under :focus-visible; check the focused rect is not intersected by position fixed or sticky elements above it. -## 40. `modern.every-state-is-designed` - -Empty, sparse, dense, error and loading states are designed; skeletons match final layout and long content never overflows. - -- **Source:** Vercel Labs, *Web Interface Guidelines — AGENTS.md* — https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/AGENTS.md -- **In their words:** "MUST: Design empty/sparse/dense/error states" -- **Do:** An empty state with a primary create action; Skeletons sized like real rows; min-w-0 and truncation on flex children -- **Don't:** Blank screens on empty arrays; Spinners that reflow content; Overflow from long strings -- **Look at:** Render with [], with 1 item, with 500 items and with a 300-character title; assert no horizontal overflow, no overlap, an actionable control in the empty state, and skeleton-to-loaded CLS under a threshold. - -## 41. `modern.numbers-and-text-do-not-shift-layout` - -Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. - -- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ -- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" -- **Do:** tabular-nums in tables, timers and prices; width and height on every img -- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover -- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. -- **Unless:** Prose numbers - -## 42. `modern.shadows-share-one-light-source` +## 41. `modern.shadows-share-one-light-source` All shadows on a page share one light direction and offset ratio, layered and tinted toward the background hue. @@ -473,7 +471,7 @@ All shadows on a page share one light direction and offset ratio, layered and ti - **Look at:** Collect every computed box-shadow; fail when offset signs mix across the page or distinct shadow strings exceed about 6; warn when shadow hue is far from the background hue with alpha above 0.5. - **Unless:** Inset shadows for sunken fields; Glows meant as glows -## 43. `modern.defaults-are-decisions-you-inherited` +## 42. `modern.defaults-are-decisions-you-inherited` A library or AI default is someone else's decision; commit to one written aesthetic direction instead of inheriting it. @@ -483,3 +481,201 @@ A library or AI default is someone else's decision; commit to one written aesthe - **Don't:** Untouched library defaults; Clean and modern as a brief; The AI-look constellation of purple gradient, Inter, identical cards, glass glow and bounce hover - **Look at:** Score the constellation: purple-to-blue/cyan gradients warn, Inter or Roboto with no display face notes, 3+ identical icon+h3+p cards note, backdrop-filter plus glow notes, gradient text on numerals warns, overshoot cubic-bezier on hover warns. - **Unless:** A purple brand is allowed to be purple; the tell is the constellation and the absence of a decision, not any one colour + +## 43. `color.status-colours-keep-their-meaning` + +Each status colour has one meaning across the product (critical for errors and blocked actions, warning for what needs attention, success for what went well, info for tips) and is never borrowed for promotion or decoration. + +- **Source:** Shopify Polaris, *Colors: Palettes and roles (Critical, Success)* — https://polaris.shopify.com/design/colors/palettes-and-roles +- **In their words:** "Elements using critical must convey messaging that implies that an action is impossible, blocked, or has resulted in an error." +- **Do:** Map error, warning, success and info to named roles or tokens and use them only in those roles; Reserve the critical red for errors, blocked actions and destructive buttons; Use the info role, not warning or critical, for tips and announcements +- **Don't:** A sale or 'new' badge in the error red; Success green used to entice or to advertise an offer; Warning colour for 'coming soon' or 'under construction' messaging; Two different reds meaning error on different screens +- **Look at:** Find the colour the page uses for error text (an element with role=alert, aria-invalid's described-by message, or a class/token named error/critical/danger) and the success colour likewise. Count painted elements (text, fill or border) whose colour equals that error or success colour, within a ΔE of 3, and that are neither a validation message, an invalid field, a status badge of that meaning, nor a destructive action. +- **Unless:** Brand colours that happen to be red, as long as a separate, distinct error red is used for errors; Data visualisations where a series colour coincides with a status hue but no status is implied (prefer avoiding it) + +## 44. `color.from-tokens-not-hex` + +Every colour on the page comes from the design system's named tokens or palette functions, never from hex values copied into components. + +- **Source:** GOV.UK Design System (Government Digital Service), *Styles: Colour* — https://design-system.service.gov.uk/styles/colour/ +- **In their words:** "Do not copy the specific hexadecimal (hex) colour values." +- **Do:** Reference colour by role token (brand, text, error, border) rather than by value; Use palette colours (tints and shades of a few families) for supporting elements; Use a functional token only in the context it is designed for +- **Don't:** Hex literals in component styles; Near-duplicate colours (#1d70b8 next to #1d70b9) created by eye-dropping; Using the error token as a general red +- **Look at:** Collect every computed color, background-color, border-*-color, outline-color and fill/stroke of painted elements, and every value of CSS custom properties declared on :root (and on any theme selector). Count distinct painted colours that match no custom-property value (exact RGBA after resolution), and count pairs of painted colours closer than ΔE 2 that are not identical. +- **Unless:** Images, illustrations and embedded third-party widgets; Browser defaults on unstyled native controls; GOV.UK: palette colours (not functional ones) are allowed for illustrations and custom components + +## 45. `color.controls-and-graphics-3-to-1` + +The parts that show a control is there and what state it is in (input borders, checkbox boxes and ticks, toggle tracks, icon-only buttons, meaningful chart marks) contrast at least 3:1 with the colours next to them. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "Unless the control is inactive, any visual information provided that is necessary for a user to identify that a control is present and how to operate it must have a minimum 3:1 contrast ratio with the adjacent colors." +- **Do:** Input borders at 3:1 against the background the input sits on, or a filled input background at 3:1; Checkbox ticks and radio dots at 3:1 against the box; Standalone icons at 3:1 against their background; Avoid very thin lines that anti-alias below the nominal ratio +- **Don't:** Pale grey input borders (#ddd on white is about 1.4:1); A selected state shown only by a faint tint; Hover effects that lower a control's contrast with its surroundings +- **Look at:** For each visible input, select, textarea, checkbox, radio, [role=switch] and icon-only button (no visible text): compute the contrast ratio between the colour that identifies it (border colour, or its own background when it has no border, or the icon fill) and the background behind it; count those below 3:1, unrounded. Disabled controls are skipped. +- **Unless:** Inactive (disabled) controls are exempt; A control identified by its visible text needs no contrasting boundary; Logos and decorative graphics; Appearance determined by the browser and not modified by the author + +## 46. `color.focus-ring-contrasts-with-its-surroundings` + +The focus indicator contrasts at least 3:1 with whatever it is drawn against: the page background for an outer ring, the component's own colours for an inner one. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast, Relationship with Focus Visible (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." +- **Do:** An outer ring that contrasts with the page background; A two-colour ring (dark and light) that holds on any background; A thick indicator rather than a 1px one +- **Don't:** A yellow outer ring on a white page; A focus border that changes hue inside the component without contrasting with its fill; Focus shown only by a background tint change +- **Look at:** Tab to each focusable control; diff the focused and unfocused screenshots of the control's box padded by a few pixels; for the changed pixels, take their colour and the colour of the unchanged pixels adjacent to them (page background outside, component fill inside). Count controls where no changed region reaches 3:1 against its adjacent colour. page.focus-visible checks only that some pixel changes; this checks that the change can be seen. +- **Unless:** Unmodified browser default focus styles; WCAG does not compare focused and unfocused states with each other; a background-only change is out of scope for 1.4.11 but fails Use of Color + +## 47. `writing.one-label-per-action` + +One concept, one word: controls that do the same thing carry the same label everywhere, and controls that do different things never share a label. + +- **Source:** GitHub Primer, *Accessibility guide: Descriptive buttons, 'How to test names'* — https://primer.style/guides/accessibility/descriptive-buttons +- **In their words:** "When buttons perform the same action, they have the same name." +- **Do:** Keep a terminology list of preferred words and words not to use for the product; Pick one verb per action (Delete or Remove, not both for the same thing) and reuse it across pages; Add the object to disambiguate repeated actions ('Remove Apples', 'Remove Pears') +- **Don't:** Synonyms for one action across screens: 'Save' here, 'Update' there, 'Apply' elsewhere; Identical labels for different actions on the same page; Naming the same object two ways ('workspace' and 'project') in one product +- **Look at:** Across the journey's pages, collect (accessible name, action) pairs for buttons and links, where action is the form action/href/handler target. Count names that map to two or more different actions on one page, and actions reached by two or more different names across pages; also flag known synonym pairs present together (save/update/apply, delete/remove, sign in/log in, cart/basket). Count must be 0. +- **Unless:** Delete and Remove may coexist when they mean different things (destroy vs take out of a collection), as Carbon defines them — then each must be used only for its own meaning + +## 48. `writing.sentence-case-ui-text` + +Labels, buttons, headings, tabs and table headers are written in sentence case; capitals only for the first word, proper nouns and acronyms — never title case or all caps for emphasis. + +- **Source:** IBM Carbon Design System, *Content guidelines: Writing style, Capitalization* — https://carbondesignsystem.com/guidelines/content/writing-style/ +- **In their words:** "Use sentence–case capitalization for all UI text elements." +- **Do:** 'First name', 'Email address', 'Save changes'; Refer to a UI element with the capitalization it has in the UI ('the My network page'); Use bold or italic, not capitals, for emphasis +- **Don't:** Title Case Buttons Like 'Save Your Changes'; ALL CAPS text set in the markup (as opposed to a tracked small label styled with text-transform); Capitalizing feature names to mark them as special +- **Look at:** For each button, label, th, tab and h1–h6 with three or more words, take the rendered text (after text-transform) and count those where two or more non-initial words that are not acronyms or in the page's proper-noun list start with a capital (title case), or where the whole text of four or more words is upper case. Count must be 0. +- **Unless:** Proper nouns, product names and acronyms keep their capitals; Mailchimp's own guide uses title case for page titles, menu names and global navigation — a house style, decide one and keep it; Short tracked overline labels in caps are a typographic choice; the four-word floor leaves them alone + +## 49. `writing.dates-numbers-units-for-the-reader` + +Dates spell out the month, numbers are numerals with thousands separators, and units sit a space after their number — formatted in the reader's locale, never as an ambiguous all-numeric date or a raw machine value. + +- **Source:** Shopify Polaris, *Content: Grammar and mechanics, 'Numbers, dates, and currency'* — https://polaris.shopify.com/content/grammar-and-mechanics +- **In their words:** "Use the month’s full name. If there isn’t enough space, use 3-letter abbreviations. Don’t write dates with numerals only." +- **Do:** 'December 11, 2024' or 'Dec 11, 2024' (in the reader's locale order); Numerals, not words: 'You have 5 orders to fulfill'; Thousands separators: '12,000'; A space between number and unit: '3.4 lb', '2 kg'; Currency code after the amount when currencies can be confused: '$10,000 USD'; Format with Intl.DateTimeFormat / Intl.NumberFormat for the user's locale +- **Don't:** All-numeric dates like '12/11/24'; ISO timestamps or epoch values shown raw ('2024-12-11T09:30:00Z'); Ordinals in dates ('January 23rd'); Unit glued to the number ('3.4lb'); Shortened numbers like '12 k' where the exact value matters +- **Look at:** Scan visible text nodes. Count matches of all-numeric dates (\b\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}\b), raw ISO timestamps (\d{4}-\d{2}-\d{2}T\d{2}:), integers of 5+ digits with no separator outside codes/IDs, and numbers glued to a unit (\d(kg|lb|cm|mm|km|mi|ml|oz)\b). Count must be 0. +- **Unless:** Polaris notes these are American English base rules and dates, numbers and measurements should be localized automatically — the target is the reader's locale, not US format; Identifiers, codes, SKUs and years are not quantities and take no separator; Dense data tables may use compact numeric dates if the format is unambiguous for the locale and stated in the column header + +## 50. `data-display.no-tables-for-layout` + +A table is for comparing data in rows and columns, never for arranging content on the page; layout belongs to the grid. + +- **Source:** GOV.UK Design System (Government Digital Service), *Table — When not to use this component* — https://design-system.service.gov.uk/components/table/ +- **In their words:** "Never use the table component to layout content on a page." +- **Do:** CSS grid or flex for page and dashboard layout; A list, cards or a summary list for items that do not share columns; Tables only where every row has the same fields +- **Don't:** A table that positions a sidebar, form or dashboard tiles; Table cells holding headings, paragraphs or whole forms; role=presentation on a table that actually holds tabular data +- **Look at:** Count table elements that have no th and either contain headings, more than one paragraph per cell, form fieldsets or nested tables, or have a single row whose cells hold unrelated content blocks. +- **Unless:** HTML email, where tables are still the only reliable layout tool + +## 51. `data-display.text-left-headers-follow-their-column` + +Text cells are left-aligned, nothing in a data table is centred, and each column header takes the same alignment as the data below it. + +- **Source:** W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.), *Tables Tutorial — Tips and Tricks: Alignment* — https://www.w3.org/WAI/tutorials/tables/tips/ +- **In their words:** "Align text to the left and numeric data to the right (in left-to-right languages), so that people using larger text sizes or smaller screens will be able to find it. … It’s helpful to give column headers the same alignment as the data in the cells below." +- **Do:** Left-align (start-align) textual cells and their headers; Give each th the same horizontal alignment as the cells of its column; Mirror alignment in right-to-left languages +- **Don't:** Centre-aligned text or number columns; A left-aligned header above a right-aligned numeric column; Centred headers as a table-wide default +- **Look at:** For each table column: count th whose computed text-align differs from the dominant text-align of the td in the same column, and count td/th whose computed text-align is center (excluding cells that hold only a checkbox, icon or status badge). +- **Unless:** A column holding only a checkbox, icon or single glyph may be centred; Right-to-left scripts reverse the sides + +## 52. `data-display.one-unit-and-format-per-column` + +Every value in a column uses the same unit, precision and format; the unit is named once in the header, not repeated in each cell. + +- **Source:** U.S. Web Design System (GSA), *Table — Usability guidance* — https://designsystem.digital.gov/components/table/ +- **In their words:** "Predictably format columns. Take care not to vary units or formatting within the same column. Instead, normalize values so they can be easily compared." +- **Do:** Normalise a column to one unit (all days, not days and weeks); Keep the same number of decimal places in every row of a column; Put the unit or symbol in the column header ('Temperature °C') rather than in every cell; Use one date format and one thousands separator throughout the table +- **Don't:** Mixing '3 days' and '2 weeks' in one column; '12.5', '12.50' and '12.500' in the same column; Abbreviating some values ('1.2k') and not others in the same column +- **Look at:** For each column of numeric td, count distinct decimal-place counts, distinct unit suffixes or prefixes, and distinct date formats; any column with more than one is a failure. +- **Unless:** A column deliberately mixing magnitudes where the unit changes are labelled per cell and sorting is by underlying value — rare, and worth questioning + +## 53. `data-display.summary-list-for-key-value-facts` + +A set of facts about one thing — label and value pairs — is shown as a summary list (dl with dt and dd), not as a table with no headers or as loose text. + +- **Source:** GOV.UK Design System (Government Digital Service), *Summary list — When to use / When not to use this component* — https://design-system.service.gov.uk/components/summary-list/ +- **In their words:** "Use a summary list to show information as a list of key facts. … only use it to present information that has a key and at least one value." +- **Do:** dl with dt (the key) and dd (the value) for record details, metadata and check-your-answers pages; A row action ('Change') whose accessible name includes the key ('Change name'); Headings or cards to separate several summary lists on one page +- **Don't:** A two-column table without th used to show one record's fields; Key–value pairs set as 'Label: value' runs in a paragraph; A summary list for genuinely tabular data or a plain list of items +- **Look at:** Where the screen shows the fields of a single record (a profile, an order, an item's metadata, a check-answers page), are the label–value pairs marked up as dl/dt/dd with each key visually distinct from its value — and is tabular data comparing several records in a table rather than a summary list? +- **Unless:** Two or three facts inside a card may be inline text if they are not scanned as a set + +## 54. `forms.error-summary-at-the-top` + +After a failed submit, show an error summary at the top of the page that takes focus and links each error to its field — even when there is only one error. + +- **Source:** GOV.UK Design System, *Error summary component* — https://design-system.service.gov.uk/components/error-summary/ +- **In their words:** "Always show an error summary when there is a validation error, even if there’s only one." +- **Do:** A summary above the h1 (below any back link) with a heading such as 'There is a problem'; Move keyboard focus to the summary when it appears; One link per error, pointing at the field (or the first field of a date or radio group); Word each summary item exactly like the message beside its field; Prefix the page <title> with 'Error: ' +- **Don't:** Errors shown only beside fields far down a long form, with focus left on the submit button; A summary of plain text with no links to the fields; A toast or banner that disappears before it can be read +- **Look at:** Submit the form empty. Within 1 s: is there an element above the first h1 of main that has focus (document.activeElement inside it), contains one link per invalid field, and does each link's href resolve to the id of an invalid input? Does document.title start with 'Error'? +- **Unless:** Primer suggests the interactive summary only for 3 or more errors, and otherwise focusing the first invalid field; A one-field form (search, single email sign-up) can rely on the message beside the field + +## 55. `forms.validate-when-the-user-is-done` + +Do not show an error while the user is still typing; validate when they try to continue, and only add earlier validation where research shows it helps. + +- **Source:** GOV.UK Design System, *Recover from validation errors pattern — When to tell the user about validation errors* — https://design-system.service.gov.uk/patterns/validation/ +- **In their words:** "Generally speaking, avoid validating the information in a field before the user has finished entering it. This sort of validation can cause problems - especially for users who type more slowly." +- **Do:** Validate on Continue or Submit; After a failed submit, update a field's error live once the user fixes it (Primer); A character count is the accepted exception: warn as the limit is passed +- **Don't:** An error that appears on the first keystroke of an email or phone field; Red borders on an untouched form at load; Browser-native HTML5 validation bubbles in place of designed messages +- **Look at:** Focus an email or formatted field and type one character, keeping focus. Wait 1 s. Does any error message, aria-invalid=true or error colour appear before blur or submit? Also load the form fresh: are any fields already marked invalid? +- **Unless:** GOV.UK goes further and says not to validate on blur either; Baymard (usability.inline-validation-after-leaving-field) recommends blur validation for hard fields — the schools agree only on 'not while typing'; Primer allows validating as the user types once the field has already been flagged invalid, so the error clears as soon as it is fixed + +## 56. `forms.keep-answers-after-an-error` + +When a submit fails, show the form again with every answer the user gave still in it — the failing ones and the passing ones. + +- **Source:** GOV.UK Design System, *Error message component — How it works* — https://design-system.service.gov.uk/components/error-message/ +- **In their words:** "Do not clear any form fields when showing the Error message component. Keep both passing and failing answers." +- **Do:** Re-render server-side errors with submitted values filled in; Keep the failing value so the user can see and edit what went wrong; Pre-populate fields when the user goes back to change an answer +- **Don't:** A reload that empties the form after a server error; Clearing the field that failed; Clearing password-adjacent fields such as name and email along with the password +- **Look at:** Fill every field with valid values except one, submit, and after the error page paints read each input's value: how many fields that had a value now read empty? +- **Unless:** Password and card security code fields may be cleared for security, and the message should say so + +## 57. `forms.mark-optional-fields-in-words` + +Ask mostly required questions and label the exceptions '(optional)' in words; do not mark required fields with asterisks. + +- **Source:** GOV.UK Design System, *Question pages pattern* — https://design-system.service.gov.uk/patterns/question-pages/ +- **In their words:** "in most contexts, add ‘(optional)’ to the labels of optional fields" +- **Do:** '(optional)' in the label, or in the legend for a radio or checkbox group; Removing optional questions before marking them; One convention across the whole service +- **Don't:** Asterisks on mandatory fields; Optional fields that look identical to required ones; Required and optional marked differently on different pages +- **Look at:** Submit the form with every field empty and note which fields raise no error (optional). Count optional fields whose label or legend text lacks 'optional', and labels that contain '*'. +- **Unless:** USWDS instead marks required fields with a red asterisk explained by a note at the top, and also labels optional fields '(optional)' — the schools agree that optional fields say so in words; Primer and USWDS exempt one-field forms and login forms, where every field is plainly required + +## 58. `forms.autocomplete-names-the-purpose` + +Every field that asks about the user — name, email, phone, address, postcode, birthday, card — carries the matching autocomplete token so browsers and assistive tech can fill and label it. + +- **Source:** W3C Web Accessibility Initiative, *Understanding WCAG 2.2 Success Criterion 1.3.5: Identify Input Purpose* — https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html +- **In their words:** "Use code to indicate the purpose of common inputs, where technology allows." +- **Do:** autocomplete='name' or 'given-name' and 'family-name' on name fields; 'email', 'tel', 'postal-code', 'street-address' or 'address-line1', 'bday-day'/'bday-month'/'bday-year'; 'shipping' and 'billing' section tokens on order forms; autocomplete='off' on the form, if needed, while each field still declares its purpose +- **Don't:** autocomplete='off' on personal fields with no purpose token; Personal-data fields with no autocomplete attribute; A token that does not match the field (email token on a phone field) +- **Look at:** For every input, select and textarea whose label matches name, email, phone, address, postcode/ZIP, city, country, date of birth, card number or expiry: count those whose autocomplete attribute is missing, 'off', or not a WCAG input-purpose token that matches the label. +- **Unless:** Fields about someone other than the user (a recipient's email) are outside 1.3.5; A field that accepts either username or email may carry one token or none + +## 59. `forms.hint-text-is-short-and-linked` + +Hint text sits between the label and the input, is one short sentence with no links, and is tied to the input with aria-describedby; longer explanation goes in the page body. + +- **Source:** GOV.UK Design System, *Text input component — Hint text* — https://design-system.service.gov.uk/components/text-input/ +- **In their words:** "Keep hint text to a single short sentence, without any full stops." +- **Do:** Hint placed after the label and before the input; The input's aria-describedby lists the hint's id (and the error's, when shown); Hints that say where to find the answer or how it will be used; A statement heading plus body text when a question needs a long explanation +- **Don't:** Links inside hint text; Multi-sentence paragraphs as hints; Hints placed below the input where they are read after typing; Hints in a tooltip behind an icon +- **Look at:** For every element used as a hint (referenced by an input's aria-describedby, or styled as hint between label and input): count hints containing an <a>, hints longer than one sentence or about 120 characters, hints rendered below their input, and visible hints not referenced by their input's aria-describedby. +- **Unless:** A format example ('For example, QQ 12 34 56 C') may follow the sentence; "About 120 characters" in "look at" is uxcli's reading of "a single short sentence", not a number the source gives. + +## 60. `forms.ask-only-what-you-need` + +Every question on the form has a named use; drop the ones nobody can justify, and never ask for the same thing twice in one journey. + +- **Source:** GOV.UK Design System, *Question pages pattern* — https://design-system.service.gov.uk/patterns/question-pages/ +- **In their words:** "You should make sure you know why you’re asking every question and only ask users for information you really need." +- **Do:** A question protocol: for each field, who uses the answer and what for; Reuse an earlier answer by pre-filling it rather than asking again; Let 'I do not know' be an answer where it is a valid one; Say why a sensitive question is asked, in its hint +- **Don't:** 'Nice to have' fields such as title, gender or 'how did you hear about us' with no stated use; Asking for an email or address a second time in the same journey; Confirm-email fields that make the user type the address twice, instead of playing it back for checking +- **Look at:** List every field on the form. For each, can the team name who reads the answer and what decision it changes? How many fields have no answer, and how many repeat information given earlier in the journey? +- **Unless:** Legal or regulatory questions whose use is mandated, which should still say why in a hint diff --git a/skills/uxcli/lenses/transaction.json b/skills/uxcli/lenses/transaction.json index 0d9887e..3095dad 100644 --- a/skills/uxcli/lenses/transaction.json +++ b/skills/uxcli/lenses/transaction.json @@ -9,57 +9,59 @@ "note": "Same rule, also stated as: usability.hicks-fewer-choices-when-time-matters (Jon Yablonski (Laws of UX)); usability.peak-end-finish-well (Jon Yablonski (Laws of UX)); usability.von-restorff-one-emphasis (GOV.UK Design System); craft.growth-design-psychology-principles (Growth.Design (Dan Benoni, Louis-Xavier Lavallée)); modern.no-deceptive-patterns (Harry Brignull)." }, { - "viewpoint": "usability.no-false-affordances", - "note": "Same rule, also stated as: canon.rams-honest (Dieter Rams)." + "viewpoint": "usability.speak-the-users-language", + "note": "Same rule, also stated as: writing.errors-say-what-and-how-to-fix (GOV.UK Design System (Government Digital Service)); writing.plain-language-reading-level (Shopify Polaris); forms.error-message-says-how-to-fix (GOV.UK Design System); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen)." }, { "viewpoint": "usability.inline-validation-after-leaving-field", - "note": "Same rule, also stated as: usability.visibility-of-system-status (Jakob Nielsen); modern.feedback-is-local-and-optimistic (Rauno Freiberg)." + "note": "Same rule, also stated as: usability.visibility-of-system-status (Jakob Nielsen); modern.feedback-is-local-and-optimistic (Rauno Freiberg); forms.error-summary-at-the-top (GOV.UK Design System)." }, { - "viewpoint": "usability.feedback-within-a-second", - "note": "Same rule, also stated as: modern.interactions-feel-immediate-under-200ms (Rauno Freiberg)." + "viewpoint": "usability.postel-tolerant-input", + "note": "Same rule, also stated as: craft.body-16px-line-height-1-5 (Steve Schoger); modern.mobile-inputs-do-not-zoom-or-trap (Rauno Freiberg); forms.input-type-matches-the-answer (GOV.UK Design System)." }, { - "viewpoint": "usability.signifiers-make-clickable-look-clickable", - "note": "Same rule, also stated as: modern.signifiers-survive-flatness (Kate Moran (NN/G))." + "viewpoint": "usability.omit-needless-words", + "note": "Same rule, also stated as: writing.sentence-case-ui-text (IBM Carbon Design System)." }, { - "viewpoint": "usability.speak-the-users-language" + "viewpoint": "usability.fewer-checkout-fields", + "note": "Same rule, also stated as: forms.mark-optional-fields-in-words (GOV.UK Design System); forms.ask-only-what-you-need (GOV.UK Design System)." }, { "viewpoint": "usability.minimalist-no-competing-information" }, { - "viewpoint": "usability.postel-tolerant-input" + "viewpoint": "usability.no-false-affordances", + "note": "Same rule, also stated as: canon.rams-honest (Dieter Rams)." }, { - "viewpoint": "usability.omit-needless-words" + "viewpoint": "usability.help-in-context", + "note": "Same rule, also stated as: usability.placeholders-are-not-labels (Katie Sherwin, NN/g); forms.hint-text-is-short-and-linked (GOV.UK Design System)." }, { - "viewpoint": "usability.progress-indication-in-flows" + "viewpoint": "usability.feedback-within-a-second", + "note": "Same rule, also stated as: modern.interactions-feel-immediate-under-200ms (Rauno Freiberg)." }, { - "viewpoint": "usability.single-column-forms" + "viewpoint": "usability.signifiers-make-clickable-look-clickable", + "note": "Same rule, also stated as: modern.signifiers-survive-flatness (Kate Moran (NN/G))." }, { - "viewpoint": "usability.fewer-checkout-fields" + "viewpoint": "usability.clearly-marked-emergency-exit" }, { - "viewpoint": "usability.aesthetic-usability-effect-bias" + "viewpoint": "usability.progress-indication-in-flows" }, { - "viewpoint": "usability.clearly-marked-emergency-exit" + "viewpoint": "usability.single-column-forms" }, { - "viewpoint": "usability.help-in-context" + "viewpoint": "usability.aesthetic-usability-effect-bias" }, { "viewpoint": "usability.natural-mapping-and-proximity" }, - { - "viewpoint": "usability.placeholders-are-not-labels" - }, { "viewpoint": "usability.labels-above-fields" }, @@ -84,33 +86,30 @@ }, { "viewpoint": "craft.spacing-and-sizing-system", - "note": "Same rule, also stated as: canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen); modern.spacing-comes-from-a-scale (Stan Kirilov)." + "note": "Same rule, also stated as: canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.spacing-comes-from-a-scale (Stan Kirilov)." }, { "viewpoint": "craft.separation-order-space-then-lines-then-boxes" }, { - "viewpoint": "craft.body-16px-line-height-1-5", - "note": "Same rule, also stated as: modern.readable-type-sizes-and-weights (Rauno Freiberg); modern.mobile-inputs-do-not-zoom-or-trap (Rauno Freiberg)." + "viewpoint": "craft.button-hierarchy-one-primary", + "note": "Same rule, also stated as: writing.destructive-actions-name-the-consequence (IBM Carbon Design System)." }, { - "viewpoint": "craft.tap-targets-and-control-height", - "note": "Same rule, also stated as: modern.hit-targets-meet-platform-minimums (Apple)." - }, - { - "viewpoint": "craft.type-scale-few-font-sizes", - "note": "Same rule, also stated as: canon.vignelli-two-type-sizes (Massimo Vignelli)." + "viewpoint": "craft.wcag-contrast-and-dont-rely-on-colour-alone", + "note": "Same rule, also stated as: modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group)." }, { - "viewpoint": "craft.button-hierarchy-one-primary" + "viewpoint": "craft.anything-but-dropdowns", + "note": "Same rule, also stated as: modern.boring-and-familiar-beats-novel (Scott Berkun); forms.memorable-dates-as-three-fields (GOV.UK Design System)." }, { - "viewpoint": "craft.wcag-contrast-and-dont-rely-on-colour-alone", - "note": "Same rule, also stated as: modern.contrast-and-not-colour-alone (Apple)." + "viewpoint": "craft.tap-targets-and-control-height", + "note": "Same rule, also stated as: modern.hit-targets-meet-platform-minimums (Apple)." }, { - "viewpoint": "craft.anything-but-dropdowns", - "note": "Same rule, also stated as: modern.boring-and-familiar-beats-novel (Scott Berkun)." + "viewpoint": "craft.type-scale-few-font-sizes", + "note": "Same rule, also stated as: canon.vignelli-two-type-sizes (Massimo Vignelli)." }, { "viewpoint": "craft.hierarchy-is-everything-squint-test" @@ -131,6 +130,13 @@ "viewpoint": "modern.defaults-are-decisions-you-inherited", "note": "Same rule, also stated as: craft.supercharge-the-defaults (Steve Schoger (attendee notes by ynotdraw))." }, + { + "viewpoint": "modern.readable-type-sizes-and-weights" + }, + { + "viewpoint": "modern.numbers-and-text-do-not-shift-layout", + "note": "Same rule, also stated as: data-display.right-align-numbers-tabular-figures (GitHub Primer)." + }, { "viewpoint": "modern.delight-scales-with-rarity" }, @@ -147,7 +153,58 @@ "viewpoint": "modern.every-state-is-designed" }, { - "viewpoint": "modern.numbers-and-text-do-not-shift-layout" + "viewpoint": "color.status-colours-keep-their-meaning" + }, + { + "viewpoint": "color.from-tokens-not-hex" + }, + { + "viewpoint": "color.one-action-colour-apart-from-status" + }, + { + "viewpoint": "color.controls-and-graphics-3-to-1" + }, + { + "viewpoint": "color.focus-ring-contrasts-with-its-surroundings" + }, + { + "viewpoint": "color.few-families-in-proportion" + }, + { + "viewpoint": "writing.success-names-what-happened" + }, + { + "viewpoint": "writing.buttons-name-the-action" + }, + { + "viewpoint": "writing.one-label-per-action" + }, + { + "viewpoint": "writing.dates-numbers-units-for-the-reader" + }, + { + "viewpoint": "data-display.text-left-headers-follow-their-column" + }, + { + "viewpoint": "data-display.no-tables-for-layout" + }, + { + "viewpoint": "data-display.summary-list-for-key-value-facts" + }, + { + "viewpoint": "forms.group-related-inputs-in-a-fieldset" + }, + { + "viewpoint": "forms.do-not-disable-the-submit-button" + }, + { + "viewpoint": "forms.validate-when-the-user-is-done" + }, + { + "viewpoint": "forms.keep-answers-after-an-error" + }, + { + "viewpoint": "forms.autocomplete-names-the-purpose" } ] } diff --git a/skills/uxcli/lenses/transaction.md b/skills/uxcli/lenses/transaction.md index cbf3df8..177939a 100644 --- a/skills/uxcli/lenses/transaction.md +++ b/skills/uxcli/lenses/transaction.md @@ -2,7 +2,7 @@ Checkout, sign-up, booking, onboarding and multi-step forms: one task, completed once, correctly. -44 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. +61 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. ## 1. `usability.fitts-target-size-and-distance` @@ -16,16 +16,17 @@ Make targets big, space them apart, and put them where the pointer already is; t - **Unless:** Infinite targets along screen edges for mouse — size matters less at an edge; Primary CTAs, moving users, children and the elderly need larger than the minimum - **Also stated as:** usability.hicks-fewer-choices-when-time-matters (Jon Yablonski (Laws of UX)); usability.peak-end-finish-well (Jon Yablonski (Laws of UX)); usability.von-restorff-one-emphasis (GOV.UK Design System); craft.growth-design-psychology-principles (Growth.Design (Dan Benoni, Louis-Xavier Lavallée)); modern.no-deceptive-patterns (Harry Brignull). -## 2. `usability.no-false-affordances` +## 2. `usability.speak-the-users-language` -Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. +Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. -- **Source:** Hoa Loranger, NN/g, *Beyond Blue Links: Making Clickable Elements Recognizable (2015)* — https://www.nngroup.com/articles/clickable-elements/ -- **In their words:** "Avoid making non-clickable items (like headings) resemble buttons." -- **Do:** Reserve link colour and underline for links; Reserve button shape and fill for buttons -- **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons -- **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). -- **Also stated as:** canon.rams-honest (Dieter Rams). +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #2* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "The design should speak the users' language. Use words, phrases, and concepts familiar to the user, rather than internal jargon." +- **Do:** Use words the user understands without looking them up; Spell acronyms out in full on each page; Write 'for example', not 'eg' or 'ie' +- **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations +- **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. +- **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se +- **Also stated as:** writing.errors-say-what-and-how-to-fix (GOV.UK Design System (Government Digital Service)); writing.plain-language-reading-level (Shopify Polaris); forms.error-message-says-how-to-fix (GOV.UK Design System); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen). ## 3. `usability.inline-validation-after-leaving-field` @@ -37,42 +38,43 @@ Validate a hard field after the user leaves it, never on focus or while typing, - **Don't:** Validate before and while typing; Show an error on focus of an empty field; Validate only on submit - **Look at:** Drive a field: focus shows no error; typing an invalid value shows none (unless length threshold reached); blur shows an error adjacent via aria-describedby; typing a fix clears it without blur. - **Unless:** Simple fields need none; premature validation was worse than none (Wroblewski) -- **Also stated as:** usability.visibility-of-system-status (Jakob Nielsen); modern.feedback-is-local-and-optimistic (Rauno Freiberg). +- **Also stated as:** usability.visibility-of-system-status (Jakob Nielsen); modern.feedback-is-local-and-optimistic (Rauno Freiberg); forms.error-summary-at-the-top (GOV.UK Design System). -## 4. `usability.feedback-within-a-second` +## 4. `usability.postel-tolerant-input` -Paint something within 0.1 s; keep the user's flow with a response under 1 s; show a progress indicator for anything longer. +Accept every reasonable form of an input — spaces, dashes, case, whitespace — and normalise it, instead of rejecting it. -- **Source:** Jakob Nielsen, *Response Times: The 3 Important Limits (1993)* — https://www.nngroup.com/articles/response-times-3-important-limits/ (study) -- **In their words:** "0.1 second is about the limit for having the user feel that the system is reacting instantaneously … 1.0 second is about the limit for the user's flow of thought to stay uninterrupted" -- **Do:** Use a progress indicator for any action over about 1 s; Use a looped indicator for 2–9 s waits; Use a percent-done indicator for 10 s or more -- **Don't:** Leave dead time with no indicator; Show a percent-done bar that lies badly -- **Look at:** Time from input event to first paint of any change; presence of a progress element, aria-busy, <progress> or role=status update when the wait exceeds 1 s. -- **Unless:** Laws of UX claims a purposeful delay can raise perceived value — an opinion with no study cited -- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg). +- **Source:** Jon Yablonski (Laws of UX), *Postel's Law — Laws of UX* — https://lawsofux.com/postels-law/ +- **In their words:** "Be liberal in what you accept, and conservative in what you send." +- **Do:** Accept variable input and translate it to your format; Normalise on blur; Define boundaries and give clear feedback +- **Don't:** Reject spaces or dashes in card or phone numbers; Reject leading or trailing whitespace in email; Treat email as case-sensitive; Force a date format the field could parse +- **Look at:** Drive each formatted field with equivalent variants ('4111 1111 1111 1111' vs '4111111111111111', ' a@b.co ', '+44 20…' vs '02…') and compare validation outcomes. +- **Unless:** Inputs where ambiguity is dangerous, such as dates in medical or legal contexts +- **Also stated as:** craft.body-16px-line-height-1-5 (Steve Schoger); modern.mobile-inputs-do-not-zoom-or-trap (Rauno Freiberg); forms.input-type-matches-the-answer (GOV.UK Design System). -## 5. `usability.signifiers-make-clickable-look-clickable` +## 5. `usability.omit-needless-words` -Links and buttons must look clickable — colour, underline, border or fill — because weak signifiers cost measured time and fixations. +Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. -- **Source:** Kate Moran, NN/g, *Flat UI Elements Attract Less Attention and Cause Uncertainty (2017)* — https://www.nngroup.com/articles/flat-ui-less-attention-cause-uncertainty/ (study) -- **In their words:** "22% more time" -- **Do:** Make links stand out from body text; Make buttons resemble physical buttons with rectangular shapes; Apply consistent treatment throughout the site; Provide signifiers rather than rely on affordances -- **Don't:** Style linked text as static text; Use ghost buttons as the default; Use disabled buttons if avoidable; Rely on a label to say that a control is a control -- **Look at:** For each a[href], button, [role=button]: computed colour, underline, border and background versus surrounding text; a link matching body colour with no underline and no other differentiator is weak; count disabled buttons. -- **Unless:** Link position in nav menus or peripheral lists may eliminate the need for underlining (Loranger); Flat works best with low information density, traditional layouts and high-contrast targets positioned standardly (Moran) -- **Also stated as:** modern.signifiers-survive-flatness (Kate Moran (NN/G)). +- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) +- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" +- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns +- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case +- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. +- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text +- **Also stated as:** writing.sentence-case-ui-text (IBM Carbon Design System). -## 6. `usability.speak-the-users-language` +## 6. `usability.fewer-checkout-fields` -Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. +Count fields, not steps: a guest checkout needs about eight, with optional fields collapsed behind links and billing defaulted to shipping. -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #2* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "The design should speak the users' language. Use words, phrases, and concepts familiar to the user, rather than internal jargon." -- **Do:** Use words the user understands without looking them up; Spell acronyms out in full on each page; Write 'for example', not 'eg' or 'ie' -- **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations -- **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. -- **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se +- **Source:** Baymard Institute, *Checkout Optimization: minimize form fields (2024)* — https://baymard.com/blog/checkout-flow-average-form-fields (study) +- **In their words:** "The number of form fields in a checkout impacts overall usability far more than the number of steps." +- **Do:** Consolidate to a single 'Full Name' field; Hide Address Line 2 and the coupon field behind links; Default billing to shipping; Offer account creation after purchase +- **Don't:** Count steps as the metric; Show every optional field by default +- **Look at:** Count visible input, select and textarea across the checkout (excluding hidden and collapsed); optional fields shown expanded; separate first/last name fields; an open coupon field. +- **Unless:** Regulatory or fraud fields that genuinely cannot be defaulted; The minimum of 8 assumes a standard guest checkout +- **Also stated as:** forms.mark-optional-fields-in-words (GOV.UK Design System); forms.ask-only-what-you-need (GOV.UK Design System). ## 7. `usability.minimalist-no-competing-information` @@ -85,29 +87,65 @@ Everything on the screen competes with the primary goal; remove what does not se - **Look at:** At 360 px: count distinct interactive elements above the fold; whether the journey's primary action is within the first viewport; count of elements sharing the accent colour. - **Unless:** Dense expert dashboards where the irreducible information is large -## 8. `usability.postel-tolerant-input` +## 8. `usability.no-false-affordances` -Accept every reasonable form of an input — spaces, dashes, case, whitespace — and normalise it, instead of rejecting it. +Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. -- **Source:** Jon Yablonski (Laws of UX), *Postel's Law — Laws of UX* — https://lawsofux.com/postels-law/ -- **In their words:** "Be liberal in what you accept, and conservative in what you send." -- **Do:** Accept variable input and translate it to your format; Normalise on blur; Define boundaries and give clear feedback -- **Don't:** Reject spaces or dashes in card or phone numbers; Reject leading or trailing whitespace in email; Treat email as case-sensitive; Force a date format the field could parse -- **Look at:** Drive each formatted field with equivalent variants ('4111 1111 1111 1111' vs '4111111111111111', ' a@b.co ', '+44 20…' vs '02…') and compare validation outcomes. -- **Unless:** Inputs where ambiguity is dangerous, such as dates in medical or legal contexts +- **Source:** Hoa Loranger, NN/g, *Beyond Blue Links: Making Clickable Elements Recognizable (2015)* — https://www.nngroup.com/articles/clickable-elements/ +- **In their words:** "Avoid making non-clickable items (like headings) resemble buttons." +- **Do:** Reserve link colour and underline for links; Reserve button shape and fill for buttons +- **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons +- **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). +- **Also stated as:** canon.rams-honest (Dieter Rams). -## 9. `usability.omit-needless-words` +## 9. `usability.help-in-context` -Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. +The best help is none; when a field needs explaining, put a short hint beside it at the moment it is needed, not on another page. -- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) -- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" -- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns -- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case -- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. -- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #10* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "Whenever possible, present the documentation in context right at the moment that the user requires it." +- **Do:** Use hint text for help relevant to most users; Keep hint text to one short sentence without full stops; Associate the hint with its input via aria-describedby +- **Don't:** Put help only on a separate page; Put links inside hint text; Write lengthy explanations +- **Look at:** For inputs with a format expectation (date, postcode, card): a hint associated via aria-describedby and rendered adjacent, same column, within one line-height; hint length in sentences. +- **Unless:** Nielsen's own first sentence: it is best if the system needs no additional explanation +- **Also stated as:** usability.placeholders-are-not-labels (Katie Sherwin, NN/g); forms.hint-text-is-short-and-linked (GOV.UK Design System). + +## 10. `usability.feedback-within-a-second` -## 10. `usability.progress-indication-in-flows` +Paint something within 0.1 s; keep the user's flow with a response under 1 s; show a progress indicator for anything longer. + +- **Source:** Jakob Nielsen, *Response Times: The 3 Important Limits (1993)* — https://www.nngroup.com/articles/response-times-3-important-limits/ (study) +- **In their words:** "0.1 second is about the limit for having the user feel that the system is reacting instantaneously … 1.0 second is about the limit for the user's flow of thought to stay uninterrupted" +- **Do:** Use a progress indicator for any action over about 1 s; Use a looped indicator for 2–9 s waits; Use a percent-done indicator for 10 s or more +- **Don't:** Leave dead time with no indicator; Show a percent-done bar that lies badly +- **Look at:** Time from input event to first paint of any change; presence of a progress element, aria-busy, <progress> or role=status update when the wait exceeds 1 s. +- **Unless:** Laws of UX claims a purposeful delay can raise perceived value — an opinion with no study cited +- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg). + +## 11. `usability.signifiers-make-clickable-look-clickable` + +Links and buttons must look clickable — colour, underline, border or fill — because weak signifiers cost measured time and fixations. + +- **Source:** Kate Moran, NN/g, *Flat UI Elements Attract Less Attention and Cause Uncertainty (2017)* — https://www.nngroup.com/articles/flat-ui-less-attention-cause-uncertainty/ (study) +- **In their words:** "22% more time" +- **Do:** Make links stand out from body text; Make buttons resemble physical buttons with rectangular shapes; Apply consistent treatment throughout the site; Provide signifiers rather than rely on affordances +- **Don't:** Style linked text as static text; Use ghost buttons as the default; Use disabled buttons if avoidable; Rely on a label to say that a control is a control +- **Look at:** For each a[href], button, [role=button]: computed colour, underline, border and background versus surrounding text; a link matching body colour with no underline and no other differentiator is weak; count disabled buttons. +- **Unless:** Link position in nav menus or peripheral lists may eliminate the need for underlining (Loranger); Flat works best with low information density, traditional layouts and high-contrast targets positioned standardly (Moran) +- **Also stated as:** modern.signifiers-survive-flatness (Kate Moran (NN/G)). + +## 12. `usability.clearly-marked-emergency-exit` + +Every interaction has a visible, labelled way out — Cancel, Back, Escape, Undo — so a mistaken action does not trap the user. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #3* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "Users often perform actions by mistake. They need a clearly marked 'emergency exit' to leave the unwanted action without having to go through an extended process." +- **Do:** Support Undo and Redo; Show a clear Cancel or close control; Label the exit clearly and make it discoverable +- **Don't:** Open a modal with no close; Build a wizard step with no back; Ship a destructive action with no undo +- **Look at:** For each role=dialog or modal: a focusable control whose text or aria-label matches close/cancel/back, and Escape dismisses it; for each step past the first in a flow: a back control exists. +- **Unless:** Legally required interstitials — the exit must still be visible, not necessarily free of consequence + +## 13. `usability.progress-indication-in-flows` In flows of three or more steps give some sense of position; do not assume a step bar helps — GOV.UK removed one with no effect. @@ -118,7 +156,7 @@ In flows of three or more steps give some sense of position; do not assume a ste - **Look at:** Presence of a step or position element in flows with 3 or more steps, and whether it updates between steps. - **Unless:** Short flows — GOV.UK's own guidance omits indicators by default; Artificial 'endowed progress' edges toward manipulation — flag, do not recommend -## 11. `usability.single-column-forms` +## 14. `usability.single-column-forms` Forms run in one vertical column; only coherent entities like city/state/ZIP or card/expiry/CVV share a row. @@ -129,18 +167,7 @@ Forms run in one vertical column; only coherent entities like city/state/ZIP or - **Look at:** Cluster inputs by left x-coordinate; more than one column of independent inputs, not in the same fieldset or an allowed coherent row, fails. - **Unless:** Coherent-entity rows: city/state/ZIP and card number/expiry/security code -## 12. `usability.fewer-checkout-fields` - -Count fields, not steps: a guest checkout needs about eight, with optional fields collapsed behind links and billing defaulted to shipping. - -- **Source:** Baymard Institute, *Checkout Optimization: minimize form fields (2024)* — https://baymard.com/blog/checkout-flow-average-form-fields (study) -- **In their words:** "The number of form fields in a checkout impacts overall usability far more than the number of steps." -- **Do:** Consolidate to a single 'Full Name' field; Hide Address Line 2 and the coupon field behind links; Default billing to shipping; Offer account creation after purchase -- **Don't:** Count steps as the metric; Show every optional field by default -- **Look at:** Count visible input, select and textarea across the checkout (excluding hidden and collapsed); optional fields shown expanded; separate first/last name fields; an open coupon field. -- **Unless:** Regulatory or fraud fields that genuinely cannot be defaulted; The minimum of 8 assumes a standard guest checkout - -## 13. `usability.aesthetic-usability-effect-bias` +## 15. `usability.aesthetic-usability-effect-bias` Polish makes a screen look more usable than it is; discount your aesthetic impression and run the measurable checks first. @@ -151,28 +178,6 @@ Polish makes a screen look more usable than it is; discount your aesthetic impre - **Look at:** Did the evaluator judge the screen usable because it looks good? Re-check the verdict against the count-kind entries before trusting it; 'attractive' is the variable and stays taste. - **Unless:** With severe usability issues, or functionality sacrificed for aesthetics, users lose patience -## 14. `usability.clearly-marked-emergency-exit` - -Every interaction has a visible, labelled way out — Cancel, Back, Escape, Undo — so a mistaken action does not trap the user. - -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #3* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "Users often perform actions by mistake. They need a clearly marked 'emergency exit' to leave the unwanted action without having to go through an extended process." -- **Do:** Support Undo and Redo; Show a clear Cancel or close control; Label the exit clearly and make it discoverable -- **Don't:** Open a modal with no close; Build a wizard step with no back; Ship a destructive action with no undo -- **Look at:** For each role=dialog or modal: a focusable control whose text or aria-label matches close/cancel/back, and Escape dismisses it; for each step past the first in a flow: a back control exists. -- **Unless:** Legally required interstitials — the exit must still be visible, not necessarily free of consequence - -## 15. `usability.help-in-context` - -The best help is none; when a field needs explaining, put a short hint beside it at the moment it is needed, not on another page. - -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #10* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "Whenever possible, present the documentation in context right at the moment that the user requires it." -- **Do:** Use hint text for help relevant to most users; Keep hint text to one short sentence without full stops; Associate the hint with its input via aria-describedby -- **Don't:** Put help only on a separate page; Put links inside hint text; Write lengthy explanations -- **Look at:** For inputs with a format expectation (date, postcode, card): a hint associated via aria-describedby and rendered adjacent, same column, within one line-height; hint length in sentences. -- **Unless:** Nielsen's own first sentence: it is best if the system needs no additional explanation - ## 16. `usability.natural-mapping-and-proximity` Put a control next to the thing it changes and a label next to its field; gaps between groups must exceed gaps within them. @@ -184,18 +189,7 @@ Put a control next to the thing it changes and a label next to its field; gaps b - **Look at:** Distance from each label to its own input versus the nearest other input; gap within a fieldset versus gap between fieldsets; distance from last input to the submit button. - **Unless:** Left-aligned labels are acceptable if space is constrained and labels are of similar length -## 17. `usability.placeholders-are-not-labels` - -Every input has a visible label outside the field; placeholder text is never the label, the hint or the only format example. - -- **Source:** Katie Sherwin, NN/g, *Placeholders in Form Fields Are Harmful (2014)* — https://www.nngroup.com/articles/form-design-placeholders/ -- **In their words:** "Disappearing placeholder text strains users' short-term memory." -- **Do:** Give every text input a visible label; Put hints outside empty form fields -- **Don't:** Use placeholder text in place of a label; Use placeholder text for hints or examples; Hide the label visually and rely on the placeholder -- **Look at:** Any input or textarea with a placeholder and no visible associated label, or with a visually hidden label; placeholder used as the only format example. -- **Unless:** A single-field search where the button label carries the meaning is the usual carve-out; the fetched page states none explicitly - -## 18. `usability.labels-above-fields` +## 17. `usability.labels-above-fields` Stack the label above its field, left edges aligned, a few pixels apart, so label and field are read in one fixation. @@ -206,7 +200,7 @@ Stack the label above its field, left edges aligned, a few pixels apart, so labe - **Look at:** For each label/input pair: label box bottom ≤ input box top and label left edge ≈ input left edge within a few px; if side by side, the gap between label right edge and input left edge. - **Unless:** Landscape phones: switch to left-aligned to keep the field visible above the keyboard (Baymard); Left-aligned is acceptable when labels are of similar length and as close to the fields as possible (NN/g) -## 19. `usability.field-width-matches-expected-input` +## 18. `usability.field-width-matches-expected-input` Size each input to the data it expects: short boxes for year, postcode and CVV; one consistent width for variable data like email. @@ -217,7 +211,7 @@ Size each input to the data it expects: short boxes for year, postcode and CVV; - **Look at:** Rendered input width in ch of its font versus maxlength, inputmode, autocomplete token or pattern length: cc-csc wider than ~8 ch, postal-code at full width, or email narrower than ~20 ch. - **Unless:** Full-width inputs on narrow phones are the norm — check at desktop widths or compare relative widths within the form -## 20. `usability.one-thing-per-page` +## 19. `usability.one-thing-per-page` Split a public-facing form so each page asks one question, decision or piece of information, with eligibility questions first. @@ -228,7 +222,7 @@ Split a public-facing form so each page asks one question, decision or piece of - **Look at:** Count distinct questions (label groups or fieldsets) per page in a flow; count required inputs per page. - **Unless:** Be consistent, not uniform: internal expert tools and repeat users may prefer denser pages -## 21. `usability.dont-make-me-think` +## 20. `usability.dont-make-me-think` A page's purpose and controls should be self-evident; nothing on it should raise a question the user must answer before acting. @@ -238,7 +232,7 @@ A page's purpose and controls should be self-evident; nothing on it should raise - **Don't:** Raise a question the user must answer before acting - **Look at:** Look at the screen cold: is there anything you have to work out before you can act? The law is the sum of the other entries; a machine measures its symptoms, not the law. -## 22. `usability.start-with-user-needs-design-with-data` +## 21. `usability.start-with-user-needs-design-with-data` Name the actor and their need before drawing a screen, then let measured behaviour, not hunches or looks, decide what changes. @@ -248,7 +242,7 @@ Name the actor and their need before drawing a screen, then let measured behavio - **Don't:** Design to a hunch; Design to an aesthetic - **Look at:** Does the journey name its actor and need (uxcli context show), and is the design decision traceable to observed behaviour rather than a hunch or a look? -## 23. `usability.constraints-shift-complexity-to-the-system` +## 22. `usability.constraints-shift-complexity-to-the-system` Complexity that cannot be removed goes to the system, not the user: default what can be inferred, hide what is rarely needed. @@ -259,7 +253,7 @@ Complexity that cannot be removed goes to the system, not the user: default what - **Look at:** Count visible required inputs the page could default (no 'same as shipping' control, both address blocks open); presence of autocomplete tokens on address and payment inputs. - **Unless:** Tognazzini's counter-view: users resist complexity reduction and attempt harder tasks when systems get simpler -## 24. `craft.fewer-borders` +## 23. `craft.fewer-borders` Separate elements with space, a background shift, a shadow or striping before reaching for a border; too many borders make a design busy. @@ -271,7 +265,7 @@ Separate elements with space, a background shift, a shadow or striping before re - **Unless:** Form inputs and the one object a person acts on keep their border; Keylines that make disconnected content feel connected; Decorative borders as style once the count is low - **Also stated as:** canon.gestalt-common-region (Aurora Harley, NN/g); canon.rams-as-little-design-as-possible (Dieter Rams); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); canon.tufte-smallest-effective-difference (Edward Tufte); canon.tufte-one-plus-one-equals-three (Edward Tufte). -## 25. `craft.spacing-and-sizing-system` +## 24. `craft.spacing-and-sizing-system` Draw every margin, padding and gap from one scale, and make space between groups clearly larger than space within them. @@ -281,9 +275,9 @@ Draw every margin, padding and gap from one scale, and make space between groups - **Don't:** Use one-off values such as 13px or 27px; Use equal gaps across a group boundary - **Look at:** Collect all computed margin, padding and gap values above 0; report distinct values and how many are off a 4px grid; compare label-to-field gap with field-to-next-label gap. - **Unless:** Optical adjustments on icons and hanging punctuation sit a pixel or two off-scale -- **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen); modern.spacing-comes-from-a-scale (Stan Kirilov). +- **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.spacing-comes-from-a-scale (Stan Kirilov). -## 26. `craft.separation-order-space-then-lines-then-boxes` +## 25. `craft.separation-order-space-then-lines-then-boxes` Use the lightest separator that works: more space first, then a keyline or background band, and a box only for the object that is acted on. @@ -293,43 +287,7 @@ Use the lightest separator that works: more space first, then a keyline or backg - **Look at:** Between sibling groups, record which separator is used: gap at least 2x the inner gap (space), hr or border-bottom (line), bordered or shadowed wrapper (box); report the inner/outer gap ratio. - **Unless:** Dense data such as tables where zebra stripes or keylines are used; Interactive cards that are the unit of action -## 27. `craft.body-16px-line-height-1-5` - -Body copy at 16px or more with 1.5 line height; inputs at 16px or more on mobile. - -- **Source:** Steve Schoger, *Little UI Details (tweet, 1 Jun 2017)* — https://digitalsynopsis.com/design/useful-ui-ux-design-tips/ -- **In their words:** "If in doubt, 16px font with 1.5 line height is pretty good safe for body copy." -- **Do:** Set body at 16px or more, secondary about 2px smaller; Set inputs at 16px or more on mobile; Set text-heavy desktop pages at 18–24px -- **Don't:** Set body text under 16px on phones; Set inputs under 16px on iOS -- **Look at:** At a 375px viewport, computed font-size of paragraph text and of input, textarea and select; line-height divided by font-size for paragraphs. -- **Unless:** Interaction-heavy desktop pages may go to 14px; Captions sit 2px under body by design -- **Also stated as:** modern.readable-type-sizes-and-weights (Rauno Freiberg); modern.mobile-inputs-do-not-zoom-or-trap (Rauno Freiberg). - -## 28. `craft.tap-targets-and-control-height` - -Touch targets are at least 44×44; inputs and the buttons beside them share one height of 40 or 48px. - -- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html -- **In their words:** "The minimum tap target size on both iOS and Android — On iOS: 44x44pt. On Android: 48x48pt." -- **Do:** Give touch viewports hit areas of 44×44 or more; Match button height to the inputs beside it -- **Don't:** Use 26px-wide grid cells as targets; Make buttons shorter than the inputs they sit beside -- **Look at:** At 375px, getBoundingClientRect of every a, button, input and role=button: fail under 44 in width or height (under 24 for inline text links); in a form row, button and input heights within 2px. -- **Unless:** Inline text links in running prose; Dense desktop-only tools if the project commits to no touch -- **Also stated as:** modern.hit-targets-meet-platform-minimums (Apple). - -## 29. `craft.type-scale-few-font-sizes` - -Use about four font sizes from a fixed scale; reuse the default size for body, menus, lists and controls. - -- **Source:** Erik D. Kennedy, *The Responsive Website Font Size Guidelines* — https://www.learnui.design/blog/mobile-desktop-website-font-size-guidelines.html -- **In their words:** "Even the most interaction-heavy pages can typically look just fine with about 4 font sizes total." -- **Do:** Use header, default, secondary (default minus 2px) and one wildcard size; Reuse the default size across body, menus, lists and controls -- **Don't:** Invent a new size per component; Apply a strict modular or golden-ratio scale to responsive pages -- **Look at:** Count distinct computed font-size values on visible text per viewport and how many fall outside the declared scale. -- **Unless:** Sizes must stay distinguishable, so large text may step many points apart; Marketing pages may add a display size -- **Also stated as:** canon.vignelli-two-type-sizes (Massimo Vignelli). - -## 30. `craft.button-hierarchy-one-primary` +## 26. `craft.button-hierarchy-one-primary` One filled brand-colour button per view; secondaries outlined, tertiaries as text, destructive actions quiet with a confirmation step. @@ -339,8 +297,9 @@ One filled brand-colour button per view; secondaries outlined, tertiaries as tex - **Don't:** Let a green button compete with the primary; Put a big red Delete beside a small Save; Colour every link brand blue - **Look at:** Classify buttons as filled, outlined or text from computed style; fail if a view or dialog has more than one filled button of distinct hues, or a delete-labelled button is filled while the confirming action is not. - **Unless:** Segmented or toggle groups; Toolbars of equal-weight actions; A page whose only job is the destructive action, where red is primary +- **Also stated as:** writing.destructive-actions-name-the-consequence (IBM Carbon Design System). -## 31. `craft.wcag-contrast-and-dont-rely-on-colour-alone` +## 27. `craft.wcag-contrast-and-dont-rely-on-colour-alone` Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dark text, and never convey status by colour alone. @@ -350,9 +309,9 @@ Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dar - **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only - **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. - **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself -- **Also stated as:** modern.contrast-and-not-colour-alone (Apple). +- **Also stated as:** modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group). -## 32. `craft.anything-but-dropdowns` +## 28. `craft.anything-but-dropdowns` Before a dropdown, try a switch, segmented control, radios, cards, typeahead, calendar, text input or stepper. @@ -362,9 +321,33 @@ Before a dropdown, try a switch, segmented control, radios, cards, typeahead, ca - **Don't:** Use a select for five or fewer options; Use three selects for a date; Ship a 195-country select without search on mobile - **Look at:** For each select: fail at 5 or fewer options unless committed as a rarely changed default; fail 2–3 adjacent selects with day, month, year options; flag over 30 options at 375px with no typeahead. - **Unless:** Users rarely need to change the default value; There are very few options; The user is not on mobile -- **Also stated as:** modern.boring-and-familiar-beats-novel (Scott Berkun). +- **Also stated as:** modern.boring-and-familiar-beats-novel (Scott Berkun); forms.memorable-dates-as-three-fields (GOV.UK Design System). + +## 29. `craft.tap-targets-and-control-height` + +Touch targets are at least 44×44; inputs and the buttons beside them share one height of 40 or 48px. + +- **Source:** Erik D. Kennedy, *100 Things a UX/UI Designer Should Know* — https://www.learnui.design/blog/100-things-ux-ui-designer-know.html +- **In their words:** "The minimum tap target size on both iOS and Android — On iOS: 44x44pt. On Android: 48x48pt." +- **Do:** Give touch viewports hit areas of 44×44 or more; Match button height to the inputs beside it +- **Don't:** Use 26px-wide grid cells as targets; Make buttons shorter than the inputs they sit beside +- **Look at:** At 375px, getBoundingClientRect of every a, button, input and role=button: fail under 44 in width or height (under 24 for inline text links); in a form row, button and input heights within 2px. +- **Unless:** Inline text links in running prose; Dense desktop-only tools if the project commits to no touch +- **Also stated as:** modern.hit-targets-meet-platform-minimums (Apple). + +## 30. `craft.type-scale-few-font-sizes` -## 33. `craft.hierarchy-is-everything-squint-test` +Use about four font sizes from a fixed scale; reuse the default size for body, menus, lists and controls. + +- **Source:** Erik D. Kennedy, *The Responsive Website Font Size Guidelines* — https://www.learnui.design/blog/mobile-desktop-website-font-size-guidelines.html +- **In their words:** "Even the most interaction-heavy pages can typically look just fine with about 4 font sizes total." +- **Do:** Use header, default, secondary (default minus 2px) and one wildcard size; Reuse the default size across body, menus, lists and controls +- **Don't:** Invent a new size per component; Apply a strict modular or golden-ratio scale to responsive pages +- **Look at:** Count distinct computed font-size values on visible text per viewport and how many fall outside the declared scale. +- **Unless:** Sizes must stay distinguishable, so large text may step many points apart; Marketing pages may add a display size +- **Also stated as:** canon.vignelli-two-type-sizes (Massimo Vignelli). + +## 31. `craft.hierarchy-is-everything-squint-test` Squint: the most important thing must catch the eye first and the least important last; one element dominates each screen. @@ -375,7 +358,7 @@ Squint: the most important thing must catch the eye first and the least importan - **Look at:** Given a declared primary action, score every interactive element as area x contrast x font-weight factor; the declared primary must rank first, and no set of buttons may sit within 10% of each other. - **Unless:** Page titles are the only element styled all-out up-pop; A browsing page such as a gallery may have no single most important thing -## 34. `craft.greys-dont-have-to-be-grey-never-use-black` +## 32. `craft.greys-dont-have-to-be-grey-never-use-black` Tint the neutral scale toward the brand hue and never use pure black for text. @@ -386,7 +369,7 @@ Tint the neutral scale toward the brand hue and never use pure black for text. - **Look at:** Count text, border and background colours with saturation 0 and lightness under 15%, and the share of neutral swatches with saturation exactly 0. - **Unless:** It does not always work; worth a trial; High-contrast or accessibility modes and print -## 35. `craft.grids-are-overrated-content-dictates-width` +## 33. `craft.grids-are-overrated-content-dictates-width` Let content dictate its own width and the layout follow; do not stretch components because the screen grew. @@ -397,7 +380,7 @@ Let content dictate its own width and the layout follow; do not stretch componen - **Look at:** At 360–414px viewport, every content edge sits at 16px (or one consistent gutter) from the viewport edge and nothing overflows horizontally. - **Unless:** Strict grids for posters and websites where the artistry of the composition matters more than the information -## 36. `canon.bringhurst-leading-is-a-rhythmic-unit` +## 34. `canon.bringhurst-leading-is-a-rhythmic-unit` The leading is the vertical unit; add and remove vertical space in multiples of it. @@ -408,7 +391,7 @@ The leading is the vertical unit; add and remove vertical space in multiples of - **Look at:** Body line-height L; margins and paddings between text blocks as multiples of L (or L/2 if the lens allows). - **Unless:** More leading for longer measures, darker faces, larger x-height and sans serifs: the ratio moves with the face -## 37. `canon.butterick-emphasis-sparingly` +## 35. `canon.butterick-emphasis-sparingly` One emphasis device at a time: bold or italic, never both; underline only links; caps only under one line and tracked. @@ -419,7 +402,7 @@ One emphasis device at a time: bold or italic, never both; underline only links; - **Look at:** Elements with font-weight ≥600 and italic; underline on non-anchors; uppercase on blocks rendering more than one line; centred multi-line paragraphs; uppercase runs with letter-spacing under 0.05em. - **Unless:** Underline for web links; All caps under one line of text -## 38. `modern.defaults-are-decisions-you-inherited` +## 36. `modern.defaults-are-decisions-you-inherited` A library or AI default is someone else's decision; commit to one written aesthetic direction instead of inheriting it. @@ -431,6 +414,29 @@ A library or AI default is someone else's decision; commit to one written aesthe - **Unless:** A purple brand is allowed to be purple; the tell is the constellation and the absence of a decision, not any one colour - **Also stated as:** craft.supercharge-the-defaults (Steve Schoger (attendee notes by ynotdraw)). +## 37. `modern.readable-type-sizes-and-weights` + +Body text sits at the platform default size, weights stay 400 or heavier, and weight never changes on hover. + +- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ +- **In their words:** "Font weights below 400 should not be used" +- **Do:** 17px body on touch, 13px minimum on desktop UI; Weights between 400 and 700; Headings at weight 500–600; Minimise the number of typefaces +- **Don't:** Body text under 11pt; font-weight 300 or lower; Weight swaps on hover or selected state +- **Look at:** Computed font-size and font-weight of every text node at a phone viewport; count distinct font-family stacks and warn above 2. +- **Unless:** Captions and legal text may sit at the platform minimum; Display headings may use light weights at large sizes + +## 38. `modern.numbers-and-text-do-not-shift-layout` + +Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. + +- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ +- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" +- **Do:** tabular-nums in tables, timers and prices; width and height on every img +- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover +- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. +- **Unless:** Prose numbers +- **Also stated as:** data-display.right-align-numbers-tabular-figures (GitHub Primer). + ## 39. `modern.delight-scales-with-rarity` Spend delight on rare moments, keep daily actions plain, and never let an element visibly duplicate itself during a transition. @@ -484,13 +490,200 @@ Empty, sparse, dense, error and loading states are designed; skeletons match fin - **Don't:** Blank screens on empty arrays; Spinners that reflow content; Overflow from long strings - **Look at:** Render with [], with 1 item, with 500 items and with a 300-character title; assert no horizontal overflow, no overlap, an actionable control in the empty state, and skeleton-to-loaded CLS under a threshold. -## 44. `modern.numbers-and-text-do-not-shift-layout` +## 44. `color.status-colours-keep-their-meaning` -Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. +Each status colour has one meaning across the product (critical for errors and blocked actions, warning for what needs attention, success for what went well, info for tips) and is never borrowed for promotion or decoration. -- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ -- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" -- **Do:** tabular-nums in tables, timers and prices; width and height on every img -- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover -- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. -- **Unless:** Prose numbers +- **Source:** Shopify Polaris, *Colors: Palettes and roles (Critical, Success)* — https://polaris.shopify.com/design/colors/palettes-and-roles +- **In their words:** "Elements using critical must convey messaging that implies that an action is impossible, blocked, or has resulted in an error." +- **Do:** Map error, warning, success and info to named roles or tokens and use them only in those roles; Reserve the critical red for errors, blocked actions and destructive buttons; Use the info role, not warning or critical, for tips and announcements +- **Don't:** A sale or 'new' badge in the error red; Success green used to entice or to advertise an offer; Warning colour for 'coming soon' or 'under construction' messaging; Two different reds meaning error on different screens +- **Look at:** Find the colour the page uses for error text (an element with role=alert, aria-invalid's described-by message, or a class/token named error/critical/danger) and the success colour likewise. Count painted elements (text, fill or border) whose colour equals that error or success colour, within a ΔE of 3, and that are neither a validation message, an invalid field, a status badge of that meaning, nor a destructive action. +- **Unless:** Brand colours that happen to be red, as long as a separate, distinct error red is used for errors; Data visualisations where a series colour coincides with a status hue but no status is implied (prefer avoiding it) + +## 45. `color.from-tokens-not-hex` + +Every colour on the page comes from the design system's named tokens or palette functions, never from hex values copied into components. + +- **Source:** GOV.UK Design System (Government Digital Service), *Styles: Colour* — https://design-system.service.gov.uk/styles/colour/ +- **In their words:** "Do not copy the specific hexadecimal (hex) colour values." +- **Do:** Reference colour by role token (brand, text, error, border) rather than by value; Use palette colours (tints and shades of a few families) for supporting elements; Use a functional token only in the context it is designed for +- **Don't:** Hex literals in component styles; Near-duplicate colours (#1d70b8 next to #1d70b9) created by eye-dropping; Using the error token as a general red +- **Look at:** Collect every computed color, background-color, border-*-color, outline-color and fill/stroke of painted elements, and every value of CSS custom properties declared on :root (and on any theme selector). Count distinct painted colours that match no custom-property value (exact RGBA after resolution), and count pairs of painted colours closer than ΔE 2 that are not identical. +- **Unless:** Images, illustrations and embedded third-party widgets; Browser defaults on unstyled native controls; GOV.UK: palette colours (not functional ones) are allowed for illustrations and custom components + +## 46. `color.one-action-colour-apart-from-status` + +Links and primary actions share one interactive colour family, used consistently, and that colour is not the colour of errors, warnings or success. + +- **Source:** IBM Carbon Design System, *Elements: Color, Overview (Color anatomy)* — https://carbondesignsystem.com/elements/color/overview/ +- **In their words:** "The core blue family serves as the primary action color across all IBM products and experiences. Additional colors are used sparingly and purposefully." +- **Do:** One link colour, used only for links; Primary buttons in the brand or action colour; Danger colour reserved for destructive buttons, not for the primary action +- **Don't:** Links in the error red; Primary buttons in several different hues across screens; Static text coloured like links +- **Look at:** Take the computed text colour of every a[href] in running text and the background of every primary (first, filled, or type=submit) button. Count distinct hues among them (30° bins), and count links or primary buttons whose colour is within ΔE 10 of the page's error, warning or success colour. Also count non-interactive text elements painted in the link colour. +- **Unless:** Primer (GitHub) deliberately uses the success role for primary buttons; a system that states such a mapping consistently is following its own rule; Destructive primary actions (Delete account) take the danger colour on purpose; Navigation menus, where position signals the link + +## 47. `color.controls-and-graphics-3-to-1` + +The parts that show a control is there and what state it is in (input borders, checkbox boxes and ticks, toggle tracks, icon-only buttons, meaningful chart marks) contrast at least 3:1 with the colours next to them. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "Unless the control is inactive, any visual information provided that is necessary for a user to identify that a control is present and how to operate it must have a minimum 3:1 contrast ratio with the adjacent colors." +- **Do:** Input borders at 3:1 against the background the input sits on, or a filled input background at 3:1; Checkbox ticks and radio dots at 3:1 against the box; Standalone icons at 3:1 against their background; Avoid very thin lines that anti-alias below the nominal ratio +- **Don't:** Pale grey input borders (#ddd on white is about 1.4:1); A selected state shown only by a faint tint; Hover effects that lower a control's contrast with its surroundings +- **Look at:** For each visible input, select, textarea, checkbox, radio, [role=switch] and icon-only button (no visible text): compute the contrast ratio between the colour that identifies it (border colour, or its own background when it has no border, or the icon fill) and the background behind it; count those below 3:1, unrounded. Disabled controls are skipped. +- **Unless:** Inactive (disabled) controls are exempt; A control identified by its visible text needs no contrasting boundary; Logos and decorative graphics; Appearance determined by the browser and not modified by the author + +## 48. `color.focus-ring-contrasts-with-its-surroundings` + +The focus indicator contrasts at least 3:1 with whatever it is drawn against: the page background for an outer ring, the component's own colours for an inner one. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast, Relationship with Focus Visible (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." +- **Do:** An outer ring that contrasts with the page background; A two-colour ring (dark and light) that holds on any background; A thick indicator rather than a 1px one +- **Don't:** A yellow outer ring on a white page; A focus border that changes hue inside the component without contrasting with its fill; Focus shown only by a background tint change +- **Look at:** Tab to each focusable control; diff the focused and unfocused screenshots of the control's box padded by a few pixels; for the changed pixels, take their colour and the colour of the unchanged pixels adjacent to them (page background outside, component fill inside). Count controls where no changed region reaches 3:1 against its adjacent colour. page.focus-visible checks only that some pixel changes; this checks that the change can be seen. +- **Unless:** Unmodified browser default focus styles; WCAG does not compare focused and unfocused states with each other; a background-only change is out of scope for 1.4.11 but fails Use of Color + +## 49. `color.few-families-in-proportion` + +Use a few colour families in a deliberate proportion (neutral base dominant, then primary, secondary and a small accent), not an even spread of many hues. + +- **Source:** U.S. Web Design System (GSA), *Design tokens: Theme color tokens* — https://designsystem.digital.gov/design-tokens/color/theme-tokens/ +- **In their words:** "about 60% of your site’s color would be the primary color family, about 30% would be the secondary color family, and about 10% would be the accent color families" +- **Do:** Neutral base for text and most surfaces; One primary family carrying most of the colour, one secondary, a small accent; Additional colours used sparingly and for a purpose (Carbon); Start in black and white, then add colour to support the message (USWDS) +- **Don't:** Five or more saturated hue families at similar weight on one screen; An accent that covers more area than the primary; A new hue introduced for a single component +- **Look at:** Screenshot the page and bucket every non-neutral pixel (HSL saturation above about 15%) by hue into 30° bins, ignoring images and status colours. How many hue families take more than 2% of the coloured area, and does the largest one carry most of it while the smallest (accent) stays near a tenth? +- **Unless:** USWDS: the proportions are for non-base colours; neutral text will usually dominate; Illustration, photography and data visualisation, which need their own palettes; Brands whose identity is multi-hue + +## 50. `writing.success-names-what-happened` + +A success message names the thing and what happened to it, in a few words ('Product updated'), instead of a generic 'Success!' or a wordy 'Your product has been successfully updated'. + +- **Source:** Shopify Polaris, *Toast component, Content guidelines: Message* — https://polaris.shopify.com/components/internal-only/toast +- **In their words:** "Written in the pattern of: noun + verb" +- **Do:** Object + past-tense verb: 'Collection added', 'Invoice sent'; Include the identifier when there are several (e.g. 'Issue #21 created' with a link); On the final page of a transaction, add a reference and what happens next +- **Don't:** A bare 'Success!', 'Done' or 'Saved successfully' with no object; 'Your X has been successfully Y-ed' padding; Title Case or field-colon formats like 'Discount: Saved successfully' +- **Look at:** Drive each save/create/send/delete action and capture the resulting status text (role=status, aria-live regions, toasts, banners). Count messages that contain no noun from the action's object (the button label's object or the page's entity name), or that are only a generic word {success, done, saved, ok, complete}, or that exceed 8 words for a toast. Count must be 0. +- **Unless:** When the result is already obvious on screen (the edited item reappears updated, or the user lands on the new item), Primer says no extra success message is needed — screen readers still need it announced; Confirmation pages at the end of a transaction carry more: reference number and what happens next + +## 51. `writing.buttons-name-the-action` + +A button's label is a verb (plus its object when needed) that says what happens when it is pressed; vague labels like OK, Done, Submit or Yes are replaced whenever a specific action exists. + +- **Source:** IBM Carbon Design System, *Dialog pattern, anatomy: Actions* — https://carbondesignsystem.com/patterns/dialog-pattern/ +- **In their words:** "Use descriptive words for the actions like Add, Delete, Save and avoid vague words like Done or OK." +- **Do:** Start the label with a verb: 'Save and continue', 'Send invoice', 'Add product'; Name the object when the verb alone is ambiguous on that screen; Make the dialog title and its confirming button use the same verb +- **Don't:** 'OK', 'Done', 'Submit', 'Yes', 'Go', 'Confirm' where a specific action exists; Labels that describe the button rather than the action ('Button', 'Click'); Icon-only buttons without an accessible name +- **Look at:** List every visible button and input[type=submit] with its accessible name. Count those whose trimmed, lower-cased name is in the generic set {ok, okay, done, submit, yes, go, confirm, click here, click, button} or is empty. Count must be 0 outside of a platform-standard alert with a single acknowledgement. +- **Unless:** A purely informational alert with one acknowledgement button may say 'OK'; GOV.UK uses 'Continue' for a question page that does not save the user's answers — a convention for multi-page forms, not a generic label + +## 52. `writing.one-label-per-action` + +One concept, one word: controls that do the same thing carry the same label everywhere, and controls that do different things never share a label. + +- **Source:** GitHub Primer, *Accessibility guide: Descriptive buttons, 'How to test names'* — https://primer.style/guides/accessibility/descriptive-buttons +- **In their words:** "When buttons perform the same action, they have the same name." +- **Do:** Keep a terminology list of preferred words and words not to use for the product; Pick one verb per action (Delete or Remove, not both for the same thing) and reuse it across pages; Add the object to disambiguate repeated actions ('Remove Apples', 'Remove Pears') +- **Don't:** Synonyms for one action across screens: 'Save' here, 'Update' there, 'Apply' elsewhere; Identical labels for different actions on the same page; Naming the same object two ways ('workspace' and 'project') in one product +- **Look at:** Across the journey's pages, collect (accessible name, action) pairs for buttons and links, where action is the form action/href/handler target. Count names that map to two or more different actions on one page, and actions reached by two or more different names across pages; also flag known synonym pairs present together (save/update/apply, delete/remove, sign in/log in, cart/basket). Count must be 0. +- **Unless:** Delete and Remove may coexist when they mean different things (destroy vs take out of a collection), as Carbon defines them — then each must be used only for its own meaning + +## 53. `writing.dates-numbers-units-for-the-reader` + +Dates spell out the month, numbers are numerals with thousands separators, and units sit a space after their number — formatted in the reader's locale, never as an ambiguous all-numeric date or a raw machine value. + +- **Source:** Shopify Polaris, *Content: Grammar and mechanics, 'Numbers, dates, and currency'* — https://polaris.shopify.com/content/grammar-and-mechanics +- **In their words:** "Use the month’s full name. If there isn’t enough space, use 3-letter abbreviations. Don’t write dates with numerals only." +- **Do:** 'December 11, 2024' or 'Dec 11, 2024' (in the reader's locale order); Numerals, not words: 'You have 5 orders to fulfill'; Thousands separators: '12,000'; A space between number and unit: '3.4 lb', '2 kg'; Currency code after the amount when currencies can be confused: '$10,000 USD'; Format with Intl.DateTimeFormat / Intl.NumberFormat for the user's locale +- **Don't:** All-numeric dates like '12/11/24'; ISO timestamps or epoch values shown raw ('2024-12-11T09:30:00Z'); Ordinals in dates ('January 23rd'); Unit glued to the number ('3.4lb'); Shortened numbers like '12 k' where the exact value matters +- **Look at:** Scan visible text nodes. Count matches of all-numeric dates (\b\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}\b), raw ISO timestamps (\d{4}-\d{2}-\d{2}T\d{2}:), integers of 5+ digits with no separator outside codes/IDs, and numbers glued to a unit (\d(kg|lb|cm|mm|km|mi|ml|oz)\b). Count must be 0. +- **Unless:** Polaris notes these are American English base rules and dates, numbers and measurements should be localized automatically — the target is the reader's locale, not US format; Identifiers, codes, SKUs and years are not quantities and take no separator; Dense data tables may use compact numeric dates if the format is unambiguous for the locale and stated in the column header + +## 54. `data-display.text-left-headers-follow-their-column` + +Text cells are left-aligned, nothing in a data table is centred, and each column header takes the same alignment as the data below it. + +- **Source:** W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.), *Tables Tutorial — Tips and Tricks: Alignment* — https://www.w3.org/WAI/tutorials/tables/tips/ +- **In their words:** "Align text to the left and numeric data to the right (in left-to-right languages), so that people using larger text sizes or smaller screens will be able to find it. … It’s helpful to give column headers the same alignment as the data in the cells below." +- **Do:** Left-align (start-align) textual cells and their headers; Give each th the same horizontal alignment as the cells of its column; Mirror alignment in right-to-left languages +- **Don't:** Centre-aligned text or number columns; A left-aligned header above a right-aligned numeric column; Centred headers as a table-wide default +- **Look at:** For each table column: count th whose computed text-align differs from the dominant text-align of the td in the same column, and count td/th whose computed text-align is center (excluding cells that hold only a checkbox, icon or status badge). +- **Unless:** A column holding only a checkbox, icon or single glyph may be centred; Right-to-left scripts reverse the sides + +## 55. `data-display.no-tables-for-layout` + +A table is for comparing data in rows and columns, never for arranging content on the page; layout belongs to the grid. + +- **Source:** GOV.UK Design System (Government Digital Service), *Table — When not to use this component* — https://design-system.service.gov.uk/components/table/ +- **In their words:** "Never use the table component to layout content on a page." +- **Do:** CSS grid or flex for page and dashboard layout; A list, cards or a summary list for items that do not share columns; Tables only where every row has the same fields +- **Don't:** A table that positions a sidebar, form or dashboard tiles; Table cells holding headings, paragraphs or whole forms; role=presentation on a table that actually holds tabular data +- **Look at:** Count table elements that have no th and either contain headings, more than one paragraph per cell, form fieldsets or nested tables, or have a single row whose cells hold unrelated content blocks. +- **Unless:** HTML email, where tables are still the only reliable layout tool + +## 56. `data-display.summary-list-for-key-value-facts` + +A set of facts about one thing — label and value pairs — is shown as a summary list (dl with dt and dd), not as a table with no headers or as loose text. + +- **Source:** GOV.UK Design System (Government Digital Service), *Summary list — When to use / When not to use this component* — https://design-system.service.gov.uk/components/summary-list/ +- **In their words:** "Use a summary list to show information as a list of key facts. … only use it to present information that has a key and at least one value." +- **Do:** dl with dt (the key) and dd (the value) for record details, metadata and check-your-answers pages; A row action ('Change') whose accessible name includes the key ('Change name'); Headings or cards to separate several summary lists on one page +- **Don't:** A two-column table without th used to show one record's fields; Key–value pairs set as 'Label: value' runs in a paragraph; A summary list for genuinely tabular data or a plain list of items +- **Look at:** Where the screen shows the fields of a single record (a profile, an order, an item's metadata, a check-answers page), are the label–value pairs marked up as dl/dt/dd with each key visually distinct from its value — and is tabular data comparing several records in a table rather than a summary list? +- **Unless:** Two or three facts inside a card may be inline text if they are not scanned as a set + +## 57. `forms.group-related-inputs-in-a-fieldset` + +Wrap each set of inputs that answer one question — radios, checkboxes, a date, an address — in a fieldset whose first child is a legend naming the question. + +- **Source:** U.S. Web Design System (USWDS), *Form component — Accessibility guidance* — https://designsystem.digital.gov/components/form/ +- **In their words:** "Group each set of thematically related controls in a fieldset element. Use the legend element to offer a label within each one." +- **Do:** fieldset + legend for every radio group and checkbox group; fieldset + legend for date (day/month/year) and multi-line address; The legend as the page h1 when the page asks only that question; '(optional)' in the legend when the whole group is optional +- **Don't:** A radio group whose question is a <p> or <div> above the radios; Radio and checkbox groups with no group name for assistive tech; A fieldset with no legend or an empty one +- **Look at:** Count radio groups (same name), checkbox groups, and day/month/year triples. How many are not inside a fieldset whose first element child is a non-empty legend (or a role=group / radiogroup with aria-labelledby pointing at visible text)? +- **Unless:** A single checkbox (I agree) needs no fieldset; Native fieldset/legend can be replaced by role=group with aria-labelledby where styling requires it + +## 58. `forms.do-not-disable-the-submit-button` + +Keep the submit button enabled while the form is incomplete; let a press show what is missing instead of greying the button out. + +- **Source:** GitHub Primer, *Forms UI pattern — Validation* — https://primer.style/product/ui-patterns/forms/overview/ +- **In their words:** "Disabled buttons are discouraged, as they don't clearly communicate what actions a user should take to complete a form." +- **Do:** An always-enabled submit that runs validation and shows the error summary; aria-disabled with an explanation where a control truly must be unavailable; Ignoring a second click while a submit is in flight, with a visible busy state +- **Don't:** A grey, unfocusable submit until every field validates; A disabled button with no text saying why; Disabled text inputs used as read-only display +- **Look at:** Load the form untouched. Is the primary submit button disabled (disabled attribute, aria-disabled=true, or pointer-events:none)? If enabled, does pressing it produce visible field errors within 1 s? +- **Unless:** Briefly blocking a second submit while the first is processing (USWDS, GOV.UK prevent-double-click); GOV.UK: only when research shows a disabled button makes the interface easier to understand + +## 59. `forms.validate-when-the-user-is-done` + +Do not show an error while the user is still typing; validate when they try to continue, and only add earlier validation where research shows it helps. + +- **Source:** GOV.UK Design System, *Recover from validation errors pattern — When to tell the user about validation errors* — https://design-system.service.gov.uk/patterns/validation/ +- **In their words:** "Generally speaking, avoid validating the information in a field before the user has finished entering it. This sort of validation can cause problems - especially for users who type more slowly." +- **Do:** Validate on Continue or Submit; After a failed submit, update a field's error live once the user fixes it (Primer); A character count is the accepted exception: warn as the limit is passed +- **Don't:** An error that appears on the first keystroke of an email or phone field; Red borders on an untouched form at load; Browser-native HTML5 validation bubbles in place of designed messages +- **Look at:** Focus an email or formatted field and type one character, keeping focus. Wait 1 s. Does any error message, aria-invalid=true or error colour appear before blur or submit? Also load the form fresh: are any fields already marked invalid? +- **Unless:** GOV.UK goes further and says not to validate on blur either; Baymard (usability.inline-validation-after-leaving-field) recommends blur validation for hard fields — the schools agree only on 'not while typing'; Primer allows validating as the user types once the field has already been flagged invalid, so the error clears as soon as it is fixed + +## 60. `forms.keep-answers-after-an-error` + +When a submit fails, show the form again with every answer the user gave still in it — the failing ones and the passing ones. + +- **Source:** GOV.UK Design System, *Error message component — How it works* — https://design-system.service.gov.uk/components/error-message/ +- **In their words:** "Do not clear any form fields when showing the Error message component. Keep both passing and failing answers." +- **Do:** Re-render server-side errors with submitted values filled in; Keep the failing value so the user can see and edit what went wrong; Pre-populate fields when the user goes back to change an answer +- **Don't:** A reload that empties the form after a server error; Clearing the field that failed; Clearing password-adjacent fields such as name and email along with the password +- **Look at:** Fill every field with valid values except one, submit, and after the error page paints read each input's value: how many fields that had a value now read empty? +- **Unless:** Password and card security code fields may be cleared for security, and the message should say so + +## 61. `forms.autocomplete-names-the-purpose` + +Every field that asks about the user — name, email, phone, address, postcode, birthday, card — carries the matching autocomplete token so browsers and assistive tech can fill and label it. + +- **Source:** W3C Web Accessibility Initiative, *Understanding WCAG 2.2 Success Criterion 1.3.5: Identify Input Purpose* — https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html +- **In their words:** "Use code to indicate the purpose of common inputs, where technology allows." +- **Do:** autocomplete='name' or 'given-name' and 'family-name' on name fields; 'email', 'tel', 'postal-code', 'street-address' or 'address-line1', 'bday-day'/'bday-month'/'bday-year'; 'shipping' and 'billing' section tokens on order forms; autocomplete='off' on the form, if needed, while each field still declares its purpose +- **Don't:** autocomplete='off' on personal fields with no purpose token; Personal-data fields with no autocomplete attribute; A token that does not match the field (email token on a phone field) +- **Look at:** For every input, select and textarea whose label matches name, email, phone, address, postcode/ZIP, city, country, date of birth, card number or expiry: count those whose autocomplete attribute is missing, 'off', or not a WCAG input-purpose token that matches the label. +- **Unless:** Fields about someone other than the user (a recipient's email) are outside 1.3.5; A field that accepts either username or email may carry one token or none diff --git a/skills/uxcli/lenses/viewpoints/color.json b/skills/uxcli/lenses/viewpoints/color.json new file mode 100644 index 0000000..b2a2046 --- /dev/null +++ b/skills/uxcli/lenses/viewpoints/color.json @@ -0,0 +1,443 @@ +{ + "schema_version": 1, + "school": "color", + "name": "Colour", + "research": "docs/lenses/color.md", + "viewpoints": [ + { + "id": "color.never-the-only-signal", + "claim": "Whatever a colour means on the page (an error, a required field, a selected state, a series) is also said in text, an icon, a shape or a lightness change of at least 3:1.", + "source": { + "author": "W3C Accessibility Guidelines Working Group", + "work": "Understanding Success Criterion 1.4.1: Use of Color (WCAG 2.2)", + "url": "https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html", + "quote": "Use information in addition to color, such as shape or text, to convey meaning." + }, + "evidence": "author", + "prefers": [ + "Pair every status colour with a word or an icon of distinct shape", + "Mark an invalid field with an error message, not only a red border", + "Show selection with a check, a weight change or a fill that contrasts 3:1 with its neighbours", + "Review the screen in greyscale or a colour-blindness simulator" + ], + "forbids": [ + "A field outlined red for invalid and green for valid with no message or icon", + "Required fields marked only by a red label", + "A legend whose only key is coloured swatches when the series are not labelled directly" + ], + "measure": { + "kind": "count", + "what": "Render the page, then render it again with a greyscale filter (filter: grayscale(1) on the root): count elements whose state or meaning (aria-invalid, aria-selected, aria-current, :checked, status badges, chart series) can no longer be told apart, i.e. two states that differ only in hue and whose relative-luminance contrast with each other is below 3:1 and that carry no differing text, icon or shape.", + "mustFail": "A form where the invalid email field differs from the valid ones only by border-color #d4351c vs #b1b4b6 (no message, no icon); in greyscale the two borders read the same.", + "mustPass": "Same form, the invalid field also shows 'Enter an email address in the correct format' above it and a thicker border." + }, + "exceptions": [ + "WCAG: a hue difference that is also a lightness difference of 3:1 or more counts as a second cue, unless the user must identify the particular colour (green for valid, red for invalid)", + "WCAG: visited-link colour is outside the author's control and does not fail on colour alone", + "Primer: colour used only as a visual flourish, carrying no information, needs no second cue" + ], + "kinds": { + "marketing": "yes", + "content": "yes", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Any page that uses colour to mean something has this obligation; it is WCAG level A.", + "agrees": [ + "craft.wcag-contrast-and-dont-rely-on-colour-alone", + "modern.contrast-and-not-colour-alone" + ] + }, + { + "id": "color.status-colours-keep-their-meaning", + "claim": "Each status colour has one meaning across the product (critical for errors and blocked actions, warning for what needs attention, success for what went well, info for tips) and is never borrowed for promotion or decoration.", + "source": { + "author": "Shopify Polaris", + "work": "Colors: Palettes and roles (Critical, Success)", + "url": "https://polaris.shopify.com/design/colors/palettes-and-roles", + "quote": "Elements using critical must convey messaging that implies that an action is impossible, blocked, or has resulted in an error." + }, + "evidence": "author", + "prefers": [ + "Map error, warning, success and info to named roles or tokens and use them only in those roles", + "Reserve the critical red for errors, blocked actions and destructive buttons", + "Use the info role, not warning or critical, for tips and announcements" + ], + "forbids": [ + "A sale or 'new' badge in the error red", + "Success green used to entice or to advertise an offer", + "Warning colour for 'coming soon' or 'under construction' messaging", + "Two different reds meaning error on different screens" + ], + "measure": { + "kind": "count", + "what": "Find the colour the page uses for error text (an element with role=alert, aria-invalid's described-by message, or a class/token named error/critical/danger) and the success colour likewise. Count painted elements (text, fill or border) whose colour equals that error or success colour, within a ΔE of 3, and that are neither a validation message, an invalid field, a status badge of that meaning, nor a destructive action.", + "mustFail": "A product page whose form error text is #d72c0d and whose '20% off' promo badge is filled #d72c0d.", + "mustPass": "Same page, the promo badge filled with the brand or info colour; #d72c0d appears only on the error message and the invalid field's border." + }, + "exceptions": [ + "Brand colours that happen to be red, as long as a separate, distinct error red is used for errors", + "Data visualisations where a series colour coincides with a status hue but no status is implied (prefer avoiding it)" + ], + "kinds": { + "marketing": "maybe", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Marketing and content pages show few statuses; anything with forms, orders or states relies on the colours keeping their meaning.", + "agrees": [] + }, + { + "id": "color.from-tokens-not-hex", + "claim": "Every colour on the page comes from the design system's named tokens or palette functions, never from hex values copied into components.", + "source": { + "author": "GOV.UK Design System (Government Digital Service)", + "work": "Styles: Colour", + "url": "https://design-system.service.gov.uk/styles/colour/", + "quote": "Do not copy the specific hexadecimal (hex) colour values." + }, + "evidence": "author", + "prefers": [ + "Reference colour by role token (brand, text, error, border) rather than by value", + "Use palette colours (tints and shades of a few families) for supporting elements", + "Use a functional token only in the context it is designed for" + ], + "forbids": [ + "Hex literals in component styles", + "Near-duplicate colours (#1d70b8 next to #1d70b9) created by eye-dropping", + "Using the error token as a general red" + ], + "measure": { + "kind": "count", + "what": "Collect every computed color, background-color, border-*-color, outline-color and fill/stroke of painted elements, and every value of CSS custom properties declared on :root (and on any theme selector). Count distinct painted colours that match no custom-property value (exact RGBA after resolution), and count pairs of painted colours closer than ΔE 2 that are not identical.", + "mustFail": "A page with 21 distinct painted colours, 8 of them not declared as any --color-* custom property, including #1d70b8 on links and #1d70b9 on a button.", + "mustPass": "Same page where every painted colour resolves to a value declared on :root and no two painted colours are within ΔE 2 of each other." + }, + "exceptions": [ + "Images, illustrations and embedded third-party widgets", + "Browser defaults on unstyled native controls", + "GOV.UK: palette colours (not functional ones) are allowed for illustrations and custom components" + ], + "kinds": { + "marketing": "yes", + "content": "yes", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Whatever the kind of UI, colour drift and broken theming come from values outside the token set.", + "agrees": [ + "color.mode-aware-tokens-in-every-theme" + ] + }, + { + "id": "color.few-families-in-proportion", + "claim": "Use a few colour families in a deliberate proportion (neutral base dominant, then primary, secondary and a small accent), not an even spread of many hues.", + "source": { + "author": "U.S. Web Design System (GSA)", + "work": "Design tokens: Theme color tokens", + "url": "https://designsystem.digital.gov/design-tokens/color/theme-tokens/", + "quote": "about 60% of your site’s color would be the primary color family, about 30% would be the secondary color family, and about 10% would be the accent color families" + }, + "evidence": "author", + "prefers": [ + "Neutral base for text and most surfaces", + "One primary family carrying most of the colour, one secondary, a small accent", + "Additional colours used sparingly and for a purpose (Carbon)", + "Start in black and white, then add colour to support the message (USWDS)" + ], + "forbids": [ + "Five or more saturated hue families at similar weight on one screen", + "An accent that covers more area than the primary", + "A new hue introduced for a single component" + ], + "measure": { + "kind": "question", + "what": "Screenshot the page and bucket every non-neutral pixel (HSL saturation above about 15%) by hue into 30° bins, ignoring images and status colours. How many hue families take more than 2% of the coloured area, and does the largest one carry most of it while the smallest (accent) stays near a tenth?" + }, + "exceptions": [ + "USWDS: the proportions are for non-base colours; neutral text will usually dominate", + "Illustration, photography and data visualisation, which need their own palettes", + "Brands whose identity is multi-hue" + ], + "kinds": { + "marketing": "yes", + "content": "yes", + "data": "maybe", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Data screens need categorical chart palettes that legitimately add hues; the chrome around them still follows the rule.", + "agrees": [ + "craft.black-and-white-first-limit-hues" + ] + }, + { + "id": "color.one-action-colour-apart-from-status", + "claim": "Links and primary actions share one interactive colour family, used consistently, and that colour is not the colour of errors, warnings or success.", + "source": { + "author": "IBM Carbon Design System", + "work": "Elements: Color, Overview (Color anatomy)", + "url": "https://carbondesignsystem.com/elements/color/overview/", + "quote": "The core blue family serves as the primary action color across all IBM products and experiences. Additional colors are used sparingly and purposefully." + }, + "evidence": "author", + "prefers": [ + "One link colour, used only for links", + "Primary buttons in the brand or action colour", + "Danger colour reserved for destructive buttons, not for the primary action" + ], + "forbids": [ + "Links in the error red", + "Primary buttons in several different hues across screens", + "Static text coloured like links" + ], + "measure": { + "kind": "count", + "what": "Take the computed text colour of every a[href] in running text and the background of every primary (first, filled, or type=submit) button. Count distinct hues among them (30° bins), and count links or primary buttons whose colour is within ΔE 10 of the page's error, warning or success colour. Also count non-interactive text elements painted in the link colour.", + "mustFail": "Body links are #1d70b8, the 'Continue' button is #d4351c, and the error summary is also #d4351c.", + "mustPass": "Body links #1d70b8, 'Continue' button the brand green #00703c, error summary #d4351c, and no static text in #1d70b8." + }, + "exceptions": [ + "Primer (GitHub) deliberately uses the success role for primary buttons; a system that states such a mapping consistently is following its own rule", + "Destructive primary actions (Delete account) take the danger colour on purpose", + "Navigation menus, where position signals the link" + ], + "kinds": { + "marketing": "yes", + "content": "yes", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Every kind has links and actions that must be recognised as such and not read as status.", + "agrees": [ + "canon.gestalt-similarity" + ] + }, + { + "id": "color.controls-and-graphics-3-to-1", + "claim": "The parts that show a control is there and what state it is in (input borders, checkbox boxes and ticks, toggle tracks, icon-only buttons, meaningful chart marks) contrast at least 3:1 with the colours next to them.", + "source": { + "author": "W3C Accessibility Guidelines Working Group", + "work": "Understanding Success Criterion 1.4.11: Non-text Contrast (WCAG 2.2)", + "url": "https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html", + "quote": "Unless the control is inactive, any visual information provided that is necessary for a user to identify that a control is present and how to operate it must have a minimum 3:1 contrast ratio with the adjacent colors." + }, + "evidence": "author", + "prefers": [ + "Input borders at 3:1 against the background the input sits on, or a filled input background at 3:1", + "Checkbox ticks and radio dots at 3:1 against the box", + "Standalone icons at 3:1 against their background", + "Avoid very thin lines that anti-alias below the nominal ratio" + ], + "forbids": [ + "Pale grey input borders (#ddd on white is about 1.4:1)", + "A selected state shown only by a faint tint", + "Hover effects that lower a control's contrast with its surroundings" + ], + "measure": { + "kind": "count", + "what": "For each visible input, select, textarea, checkbox, radio, [role=switch] and icon-only button (no visible text): compute the contrast ratio between the colour that identifies it (border colour, or its own background when it has no border, or the icon fill) and the background behind it; count those below 3:1, unrounded. Disabled controls are skipped.", + "mustFail": "A text input with a 1px #dddddd border and white fill on a white page (1.36:1).", + "mustPass": "Same input with a 2px #767676 border (4.54:1)." + }, + "exceptions": [ + "Inactive (disabled) controls are exempt", + "A control identified by its visible text needs no contrasting boundary", + "Logos and decorative graphics", + "Appearance determined by the browser and not modified by the author" + ], + "kinds": { + "marketing": "maybe", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Marketing and content pages carry few form controls; where they have any, the rule applies in full.", + "agrees": [] + }, + { + "id": "color.focus-ring-contrasts-with-its-surroundings", + "claim": "The focus indicator contrasts at least 3:1 with whatever it is drawn against: the page background for an outer ring, the component's own colours for an inner one.", + "source": { + "author": "W3C Accessibility Guidelines Working Group", + "work": "Understanding Success Criterion 1.4.11: Non-text Contrast, Relationship with Focus Visible (WCAG 2.2)", + "url": "https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html", + "quote": "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." + }, + "evidence": "author", + "prefers": [ + "An outer ring that contrasts with the page background", + "A two-colour ring (dark and light) that holds on any background", + "A thick indicator rather than a 1px one" + ], + "forbids": [ + "A yellow outer ring on a white page", + "A focus border that changes hue inside the component without contrasting with its fill", + "Focus shown only by a background tint change" + ], + "measure": { + "kind": "count", + "what": "Tab to each focusable control; diff the focused and unfocused screenshots of the control's box padded by a few pixels; for the changed pixels, take their colour and the colour of the unchanged pixels adjacent to them (page background outside, component fill inside). Count controls where no changed region reaches 3:1 against its adjacent colour. page.focus-visible checks only that some pixel changes; this checks that the change can be seen.", + "mustFail": "A button whose only focus style is outline: 2px solid #ffff00 on a white page (1.07:1).", + "mustPass": "Same button with outline: 3px solid #0b0c0c plus a #ffdd00 inner fill change (over 19:1 against white)." + }, + "exceptions": [ + "Unmodified browser default focus styles", + "WCAG does not compare focused and unfocused states with each other; a background-only change is out of scope for 1.4.11 but fails Use of Color" + ], + "kinds": { + "marketing": "yes", + "content": "yes", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Every page with a link or a control is navigated by keyboard by someone.", + "agrees": [] + }, + { + "id": "color.mode-aware-tokens-in-every-theme", + "claim": "When a product offers light and dark modes, every colour comes from tokens that change with the mode, and each default mode passes contrast on its own.", + "source": { + "author": "GitHub Primer", + "work": "Foundations: Color, UI color system (Design token categories)", + "url": "https://primer.style/foundations/color/overview", + "quote": "Base color tokens don't respect color modes and should never be used directly in code or design." + }, + "evidence": "author", + "prefers": [ + "Functional, role-named tokens (fgColor-default, bgColor-muted) that resolve per mode", + "Text, icon and border pairs checked in both modes", + "In dark themes, raised layers one step lighter than what they sit on (Carbon)", + "Honour prefers-color-scheme for the default" + ], + "forbids": [ + "Hard-coded text colours that stay dark when the background goes dark", + "Raw palette steps (gray-9, blue-5) in component styles", + "A dark mode made by inverting the page with a filter" + ], + "measure": { + "kind": "count", + "what": "Render the page with prefers-color-scheme: light and again with dark (and via the site's own theme switch if it has one). In each, run the text-contrast check and the 3:1 non-text check; count failures that exist in one mode only. Also count elements whose background colour changed between modes while their text colour did not.", + "mustFail": "Dark mode sets body background to #0d1117 but a card heading keeps color: #24292f, giving 1.3:1.", + "mustPass": "Same page where the heading uses var(--fgColor-default), resolving to #1f2328 in light and #f0f6fc in dark." + }, + "exceptions": [ + "Primer: deliberately low-contrast sub-themes (Dark Dimmed) chosen by the user need not meet contrast, provided the default light and dark themes do", + "Products with a single mode: the rule reduces to color.from-tokens-not-hex" + ], + "kinds": { + "marketing": "maybe", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "maybe", + "transaction": "maybe" + }, + "kindsWhy": "Applies only where a dark or alternate theme exists, which is common in workspace and data tools and less so elsewhere.", + "agrees": [ + "color.from-tokens-not-hex" + ] + }, + { + "id": "color.chart-palette-fits-the-data", + "claim": "In a chart, colour encodes a variable: distinct hues only for unordered categories, one hue in lightness steps for ordered values, and a single colour when every mark is the same measure.", + "source": { + "author": "IBM Carbon Design System", + "work": "Data visualization: Color palettes", + "url": "https://carbondesignsystem.com/data-visualization/color-palettes/", + "quote": "Categorical (or qualitative) palettes are best when you want to distinguish discrete categories of data that do not have an inherent correlation." + }, + "evidence": "author", + "prefers": [ + "Sequential (monochromatic) palettes for magnitude, darkest for largest in light themes", + "Diverging palettes only for data with a meaningful midpoint", + "One colour for all bars of a single series (Polaris)", + "Status colours only when the data are statuses (Primer)" + ], + "forbids": [ + "Rainbow bars for one measure", + "A gradient in place of a sequential palette", + "Reusing one colour for two variables (USWDS)" + ], + "measure": { + "kind": "question", + "what": "For each chart (svg or canvas with axes or a legend): count series and distinct mark colours. Does a single-series bar or column chart use more than one fill? Does a chart of ordered values (heat map, choropleth) use hues that are not ordered by lightness? Is any colour reused for two different series?" + }, + "exceptions": [ + "Highlighting one bar on purpose in a single-series chart", + "Charts where categories carry a conventional colour (party, team, status)" + ], + "kinds": { + "marketing": "maybe", + "content": "maybe", + "data": "yes", + "workspace": "maybe", + "shop": "no", + "transaction": "no" + }, + "kindsWhy": "Only screens that show charts; shop and transaction flows rarely do.", + "agrees": [ + "canon.tufte-graphical-integrity" + ] + }, + { + "id": "color.chart-marks-readable-without-hue", + "claim": "Chart marks contrast 3:1 with the chart background, and series are told apart by direct labels, dividers, patterns or marker shapes as well as by hue.", + "source": { + "author": "GitHub Primer", + "work": "UI patterns: Data visualization (Choosing chart colors)", + "url": "https://primer.style/ui-patterns/data-visualization", + "quote": "Chart / visualization marks need to be at a 3:1 ratio with the background." + }, + "evidence": "author", + "prefers": [ + "Label series directly on or beside the marks", + "A high-contrast divider between stacked segments", + "Dash patterns or distinct marker shapes for multiple lines", + "Palettes checked under colour-blindness simulation" + ], + "forbids": [ + "Pastel series on a white plot area", + "Five or more lines told apart only by a colour legend", + "Adjacent blue and purple series with no divider" + ], + "measure": { + "kind": "count", + "what": "For each chart: compute the contrast of every series' mark colour against the plot background and count those below 3:1. For charts with two or more series, count those with no direct series labels, no dividers between adjacent segments, and no differing dash or marker shape.", + "mustFail": "A three-line chart in #a6cee3, #b2df8a and #fdbf6f on white (all under 2:1) keyed only by a colour legend.", + "mustPass": "Same chart in #0072b2, #d55e00 and #009e73, solid, dashed and dotted, each line labelled at its right end." + }, + "exceptions": [ + "Decorative sparklines whose value is also given in text", + "WCAG: only graphical objects needed to understand the chart must meet 3:1" + ], + "kinds": { + "marketing": "maybe", + "content": "maybe", + "data": "yes", + "workspace": "maybe", + "shop": "no", + "transaction": "no" + }, + "kindsWhy": "Only screens that show charts; shop and transaction flows rarely do.", + "agrees": [ + "color.never-the-only-signal", + "color.controls-and-graphics-3-to-1" + ] + } + ] +} diff --git a/skills/uxcli/lenses/viewpoints/data-display.json b/skills/uxcli/lenses/viewpoints/data-display.json new file mode 100644 index 0000000..bd54810 --- /dev/null +++ b/skills/uxcli/lenses/viewpoints/data-display.json @@ -0,0 +1,525 @@ +{ + "schema_version": 1, + "school": "data-display", + "name": "Data display", + "research": "docs/lenses/data-display.md", + "viewpoints": [ + { + "id": "data-display.right-align-numbers-tabular-figures", + "claim": "Numbers that are compared down a column are right-aligned and set in tabular figures, so their place values line up.", + "source": { + "author": "GitHub Primer", + "work": "Data table — Best practices: Make numbers easier to compare", + "url": "https://primer.style/product/components/data-table/", + "quote": "Right-align numeric values and use the tabular-num font variant when possible." + }, + "evidence": "author", + "prefers": [ + "Right-align (text-align: right or end) every cell holding an amount, count, percentage or currency", + "Set numeric columns in font-variant-numeric: tabular-nums, or a monospace face for dense numbers", + "Right-align the header of a numeric column with its numbers" + ], + "forbids": [ + "Left- or centre-aligned columns of amounts", + "Proportional figures in a column that is meant to be compared digit by digit" + ], + "measure": { + "kind": "count", + "what": "For every td or role=cell whose text parses as a number, amount, percentage or currency, count the cells whose computed text-align is not right/end, and the numeric columns whose computed font-variant-numeric lacks tabular-nums (and whose font is not monospace).", + "mustFail": "An orders table with a Total column of '£1,204.50', '£98.00', '£12,300.00' left-aligned in a proportional font, so the decimal points zig-zag.", + "mustPass": "The same column with text-align: right and font-variant-numeric: tabular-nums on the cells and their th, decimal points in one vertical line." + }, + "exceptions": [ + "Numbers that are identifiers rather than quantities — phone numbers, postcodes, dates, order IDs — need not be right-aligned or tabular (USWDS says the same)", + "A single number standing alone, not compared down a column" + ], + "kinds": { + "marketing": "maybe", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Marketing and content pages only carry numeric columns occasionally, such as a pricing comparison or a table in an article; when they do, the rule applies.", + "agrees": [ + "craft.align-with-readability-in-mind", + "modern.numbers-and-text-do-not-shift-layout" + ] + }, + { + "id": "data-display.text-left-headers-follow-their-column", + "claim": "Text cells are left-aligned, nothing in a data table is centred, and each column header takes the same alignment as the data below it.", + "source": { + "author": "W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.)", + "work": "Tables Tutorial — Tips and Tricks: Alignment", + "url": "https://www.w3.org/WAI/tutorials/tables/tips/", + "quote": "Align text to the left and numeric data to the right (in left-to-right languages), so that people using larger text sizes or smaller screens will be able to find it. … It’s helpful to give column headers the same alignment as the data in the cells below." + }, + "evidence": "author", + "prefers": [ + "Left-align (start-align) textual cells and their headers", + "Give each th the same horizontal alignment as the cells of its column", + "Mirror alignment in right-to-left languages" + ], + "forbids": [ + "Centre-aligned text or number columns", + "A left-aligned header above a right-aligned numeric column", + "Centred headers as a table-wide default" + ], + "measure": { + "kind": "count", + "what": "For each table column: count th whose computed text-align differs from the dominant text-align of the td in the same column, and count td/th whose computed text-align is center (excluding cells that hold only a checkbox, icon or status badge).", + "mustFail": "A customers table with every th and td text-align: center, and a right-aligned Spend column under a left-aligned 'Spend' header.", + "mustPass": "Name and Email columns left-aligned with left-aligned headers; Spend right-aligned with a right-aligned header; no centred cells." + }, + "exceptions": [ + "A column holding only a checkbox, icon or single glyph may be centred", + "Right-to-left scripts reverse the sides" + ], + "kinds": { + "marketing": "maybe", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Applies wherever a data table appears; marketing and content pages have fewer of them.", + "agrees": [ + "canon.vignelli-flush-left", + "craft.center-text-only-under-three-lines", + "craft.align-with-readability-in-mind" + ] + }, + { + "id": "data-display.one-unit-and-format-per-column", + "claim": "Every value in a column uses the same unit, precision and format; the unit is named once in the header, not repeated in each cell.", + "source": { + "author": "U.S. Web Design System (GSA)", + "work": "Table — Usability guidance", + "url": "https://designsystem.digital.gov/components/table/", + "quote": "Predictably format columns. Take care not to vary units or formatting within the same column. Instead, normalize values so they can be easily compared." + }, + "evidence": "author", + "prefers": [ + "Normalise a column to one unit (all days, not days and weeks)", + "Keep the same number of decimal places in every row of a column", + "Put the unit or symbol in the column header ('Temperature °C') rather than in every cell", + "Use one date format and one thousands separator throughout the table" + ], + "forbids": [ + "Mixing '3 days' and '2 weeks' in one column", + "'12.5', '12.50' and '12.500' in the same column", + "Abbreviating some values ('1.2k') and not others in the same column" + ], + "measure": { + "kind": "count", + "what": "For each column of numeric td, count distinct decimal-place counts, distinct unit suffixes or prefixes, and distinct date formats; any column with more than one is a failure.", + "mustFail": "A Duration column reading '3 days', '2 weeks', '10 days'; a Price column reading '$4.5', '$12.00', '$7.125'.", + "mustPass": "Header 'Duration (days)' over '3', '14', '10'; header 'Price ($)' over '4.50', '12.00', '7.13'." + }, + "exceptions": [ + "A column deliberately mixing magnitudes where the unit changes are labelled per cell and sorting is by underlying value — rare, and worth questioning" + ], + "kinds": { + "marketing": "maybe", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "maybe" + }, + "kindsWhy": "Any page with a column of values; transaction pages rarely have more than a summary of a few amounts.", + "agrees": [] + }, + { + "id": "data-display.header-cells-are-th-with-scope", + "claim": "A data table marks its header cells as th and its data cells as td; when headers run both across and down, scope (or id/headers in complex tables) ties each cell to its headers.", + "source": { + "author": "W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.)", + "work": "Tables Tutorial", + "url": "https://www.w3.org/WAI/tutorials/tables/", + "quote": "Header cells must be marked up with <th>, and data cells with <td> to make tables accessible. For more complex tables, explicit associations may be needed using scope, id, and headers attributes." + }, + "evidence": "author", + "prefers": [ + "A thead row of th scope=col in every data table", + "th scope=row for the first cell when it names the row's item", + "id and headers attributes when a cell sits under more than one level of header", + "A caption that names the table" + ], + "forbids": [ + "Header rows built from td styled bold", + "Div grids that look like tables with no role=table/row/columnheader/cell semantics", + "One table holding several topics separated by extra rows of th" + ], + "measure": { + "kind": "count", + "what": "Count table (and role=table/grid) elements that have more than one row of data and no th / role=columnheader; count tables with both a header row and a header column whose th lack scope; count tables with no caption, aria-label or aria-labelledby.", + "mustFail": "<table><tr><td><b>Name</b></td><td><b>Role</b></td></tr><tr><td>Ada</td><td>Admin</td></tr></table>.", + "mustPass": "<table><caption>Team members</caption><thead><tr><th scope=col>Name</th><th scope=col>Role</th></tr></thead><tbody><tr><th scope=row>Ada</th><td>Admin</td></tr></tbody></table>." + }, + "exceptions": [ + "A simple table with one header row needs only th, not scope; scope matters once there are header rows and header columns", + "Layout tables, which should not exist (see data-display.no-tables-for-layout)" + ], + "kinds": { + "marketing": "maybe", + "content": "yes", + "data": "yes", + "workspace": "yes", + "shop": "maybe", + "transaction": "maybe" + }, + "kindsWhy": "Content pages often carry tables in articles; shop and transaction pages have them mainly in size charts, order summaries and comparisons.", + "agrees": [] + }, + { + "id": "data-display.no-tables-for-layout", + "claim": "A table is for comparing data in rows and columns, never for arranging content on the page; layout belongs to the grid.", + "source": { + "author": "GOV.UK Design System (Government Digital Service)", + "work": "Table — When not to use this component", + "url": "https://design-system.service.gov.uk/components/table/", + "quote": "Never use the table component to layout content on a page." + }, + "evidence": "author", + "prefers": [ + "CSS grid or flex for page and dashboard layout", + "A list, cards or a summary list for items that do not share columns", + "Tables only where every row has the same fields" + ], + "forbids": [ + "A table that positions a sidebar, form or dashboard tiles", + "Table cells holding headings, paragraphs or whole forms", + "role=presentation on a table that actually holds tabular data" + ], + "measure": { + "kind": "count", + "what": "Count table elements that have no th and either contain headings, more than one paragraph per cell, form fieldsets or nested tables, or have a single row whose cells hold unrelated content blocks.", + "mustFail": "A dashboard built as <table><tr><td><h2>Revenue</h2><canvas></canvas></td><td><h2>Signups</h2><form>…</form></td></tr></table>.", + "mustPass": "The same dashboard as a CSS grid of section elements, with a real table only inside the tile that lists rows of data." + }, + "exceptions": [ + "HTML email, where tables are still the only reliable layout tool" + ], + "kinds": { + "marketing": "yes", + "content": "yes", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Layout tables can turn up on any kind of page.", + "agrees": [ + "modern.cards-are-a-choice-not-a-default" + ] + }, + { + "id": "data-display.summary-list-for-key-value-facts", + "claim": "A set of facts about one thing — label and value pairs — is shown as a summary list (dl with dt and dd), not as a table with no headers or as loose text.", + "source": { + "author": "GOV.UK Design System (Government Digital Service)", + "work": "Summary list — When to use / When not to use this component", + "url": "https://design-system.service.gov.uk/components/summary-list/", + "quote": "Use a summary list to show information as a list of key facts. … only use it to present information that has a key and at least one value." + }, + "evidence": "author", + "prefers": [ + "dl with dt (the key) and dd (the value) for record details, metadata and check-your-answers pages", + "A row action ('Change') whose accessible name includes the key ('Change name')", + "Headings or cards to separate several summary lists on one page" + ], + "forbids": [ + "A two-column table without th used to show one record's fields", + "Key–value pairs set as 'Label: value' runs in a paragraph", + "A summary list for genuinely tabular data or a plain list of items" + ], + "measure": { + "kind": "question", + "what": "Where the screen shows the fields of a single record (a profile, an order, an item's metadata, a check-answers page), are the label–value pairs marked up as dl/dt/dd with each key visually distinct from its value — and is tabular data comparing several records in a table rather than a summary list?" + }, + "exceptions": [ + "Two or three facts inside a card may be inline text if they are not scanned as a set" + ], + "kinds": { + "marketing": "no", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Marketing pages rarely show a record's fields; content pages sometimes have metadata blocks.", + "agrees": [] + }, + { + "id": "data-display.long-tables-sort-and-say-so", + "claim": "Long tables let users sort the columns that have a natural order, start with one column sorted, and expose the sort state to assistive technology.", + "source": { + "author": "U.S. Web Design System (GSA)", + "work": "Table — Usability guidance", + "url": "https://designsystem.digital.gov/components/table/", + "quote": "Enable sort where useful. Add row sorting to individual columns of long tables where the data can be logically ordered either alphabetically or numerically." + }, + "evidence": "author", + "prefers": [ + "Sort controls as buttons inside the th of sortable columns", + "One column sorted on load, shown with an arrow and aria-sort on its th (Primer)", + "A polite live region that announces the new sort (USWDS)", + "A filter or search field above long tables, and pagination for very large sets" + ], + "forbids": [ + "Long tables with no sort, filter or pagination", + "A sort state shown only by an icon with no aria-sort", + "Row sorting on tables with merged (colspan/rowspan) cells" + ], + "measure": { + "kind": "count", + "what": "Count tables with more than 20 body rows (or with pagination) that have no sort control in any th and no filter/search input associated with them; among sortable tables, count those where no th carries aria-sort other than none.", + "mustFail": "A 150-row invoice table with static th text, no filter field and no pagination.", + "mustPass": "The same table with button sort controls in the Date and Amount headers, Date th carrying aria-sort=descending on load, a filter input above, and 20 rows per page." + }, + "exceptions": [ + "Short tables a user takes in at a glance", + "Tables whose row order is itself the meaning (a ranked list, a timetable) may fix the order" + ], + "kinds": { + "marketing": "no", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "maybe", + "transaction": "no" + }, + "kindsWhy": "Long tables belong to data and workspace screens; content and shop pages sometimes carry them (a reference table, an order history); marketing and transaction flows do not.", + "agrees": [] + }, + { + "id": "data-display.empty-data-explains-and-offers-next-step", + "claim": "When a table, list, chart or dashboard tile has no data, the empty space says why it is empty and what to do next, in place of the missing data — distinguishing first use, no results and error.", + "source": { + "author": "IBM Carbon Design System", + "work": "Empty states pattern — Anatomy and Where to use", + "url": "https://carbondesignsystem.com/patterns/empty-states-pattern/", + "quote": "Body: Explain clearly the next action to populate the space. You may also explain why the space is empty and include the benefit of taking this step." + }, + "evidence": "author", + "prefers": [ + "An empty state rendered where the data would be, not as a toast or on another page", + "A title, one sentence of explanation and one action that adds data, clears filters or retries", + "Different copy for no data yet, no results for this filter, and could not load", + "A tertiary action when several empty states can show at once (Carbon)" + ], + "forbids": [ + "A table header over zero rows with nothing else", + "A chart drawn with empty axes and no message", + "'No data' with no reason and no way forward", + "Several primary buttons from several empty tiles on one screen" + ], + "measure": { + "kind": "count", + "what": "Render each data region (table, list, chart, tile) with zero items: count regions that show no text, or show text but no link or button, inside the region's own bounding box; and count no-results states after filtering that offer no way to clear or change the filter.", + "mustFail": "An Invoices table rendering only its thead when the account has none; a filtered list showing a blank panel.", + "mustPass": "In the table body: 'No invoices yet — invoices you send appear here' and a 'Create invoice' button; after filtering: 'No invoices match “overdue”' with a 'Clear filters' link." + }, + "exceptions": [ + "A region that is legitimately and permanently empty for this user may collapse instead of explaining itself", + "When there is nothing the user can do, the text says how to get help instead of offering an action (Primer)" + ], + "kinds": { + "marketing": "no", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "maybe" + }, + "kindsWhy": "Data and workspace screens, carts, order histories and search results empty out; marketing pages have no user data.", + "agrees": [ + "craft.dont-overlook-empty-states-teach-by-example", + "modern.every-state-is-designed", + "usability.peak-end-finish-well" + ] + }, + { + "id": "data-display.loading-skeleton-holds-the-layout", + "claim": "Data that takes time to load is stood in for by a skeleton shaped like the real rows and columns, so nothing moves when the data arrives.", + "source": { + "author": "GitHub Primer", + "work": "Data table — Best practices: Use “skeleton” placeholders to indicate loading content", + "url": "https://primer.style/product/components/data-table/", + "quote": "Use “skeleton” placeholders to indicate loading content … The placeholder should match the real content as closely as possible. … Ideally, the height of the cell will not change once the data is loaded." + }, + "evidence": "author", + "prefers": [ + "Skeleton rows that keep the real column widths and alignment", + "Skeletons for tables, lists, cards and tiles rather than a spinner over them (Carbon)", + "One loading announcement for a group of skeletons, via aria-busy or a live region" + ], + "forbids": [ + "A centred spinner in an empty box that the table then pushes the page down from", + "Skeletons on buttons, toasts, menus or modals themselves (Carbon)", + "Skeletons whose row count or column widths bear no relation to the data" + ], + "measure": { + "kind": "count", + "what": "Throttle the data request; screenshot the loading state and the loaded state. Count layout shifts (CLS entries) attributable to the data region when data arrives, and count data regions whose loading state is a bare spinner or nothing while the request is pending past 1 s.", + "mustFail": "A 40px spinner centred in a 40px-tall div; on load a 600px table appears and pushes the footer down (CLS > 0.1).", + "mustPass": "Ten grey skeleton rows at the final row height and column widths with aria-busy=true on the table; on load the text replaces them with no shift." + }, + "exceptions": [ + "Loads under about a second may show nothing at all rather than flashing a skeleton", + "Cells whose wrapped height cannot be known in advance may change height (Primer says so)" + ], + "kinds": { + "marketing": "no", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "maybe" + }, + "kindsWhy": "Wherever data arrives after first paint; static marketing pages rarely load data.", + "agrees": [ + "modern.every-state-is-designed", + "usability.feedback-within-a-second", + "usability.visibility-of-system-status" + ] + }, + { + "id": "data-display.wrap-before-truncating-and-reveal-the-rest", + "claim": "Long cell content wraps rather than truncating; where truncation is unavoidable the full value is still available, by tooltip, title or an expanded view.", + "source": { + "author": "GitHub Primer", + "work": "Data table — Best practices: Avoid wrapping or truncation of cell content as much as possible", + "url": "https://primer.style/product/components/data-table/", + "quote": "Cells that are likely to contain long strings may choose to wrap the content. This is the preferred way to accommodate long content because it doesn't hide the content. … Content truncation is available as a last resort when column widths are set. The full content may be exposed in a tooltip." + }, + "evidence": "author", + "prefers": [ + "Wrapping long text cells", + "Truncation only with a fixed column width, and then the full text in a tooltip, title, aria-label or row expansion", + "Truncating in the middle or end only where the distinguishing part survives" + ], + "forbids": [ + "text-overflow: ellipsis with no way to read the hidden text", + "Truncating row titles that start with the same words, so every row looks identical (Polaris)", + "Truncating numbers" + ], + "measure": { + "kind": "count", + "what": "Count td, th and list-item text elements where scrollWidth > clientWidth (or line-clamp hides lines) and the element and its ancestors up to the cell carry no title, no aria-label/aria-description with the full text, no tooltip trigger and no expand control.", + "mustFail": "A Product column at 120px with white-space: nowrap; text-overflow: ellipsis showing 'Organic cotton t-…' on five rows, no title attribute.", + "mustPass": "The same column wrapping to two lines, or truncated with title='Organic cotton t-shirt, navy, XL' and a focusable tooltip." + }, + "exceptions": [ + "Free-text previews (an email snippet) whose full text is one click away on the row's detail page" + ], + "kinds": { + "marketing": "no", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "maybe" + }, + "kindsWhy": "Truncation is mostly a problem of dense tables and lists; product names in shop grids truncate often.", + "agrees": [ + "modern.every-state-is-designed" + ] + }, + { + "id": "data-display.chart-axes-are-honest", + "claim": "Bar and area charts start their value axis at zero; line charts may crop the axis to show change; gaps in the data are shown as gaps, never interpolated.", + "source": { + "author": "IBM Carbon Design System", + "work": "Data visualization — Axes and labels", + "url": "https://carbondesignsystem.com/data-visualization/axes-and-labels/", + "quote": "Always start numerical axes at zero for part-to-whole and comparisons charts, such as bar and area chart. … Never interpolate between periods when data is unavailable." + }, + "evidence": "author", + "prefers": [ + "A zero baseline on bar, column and area charts", + "A cropped y-axis only on line and scatter charts, where the trend is the message", + "Labelled start and end points around a gap in a time series", + "An explicit axis-break mark when part of an axis is skipped" + ], + "forbids": [ + "Bars rising from a non-zero baseline", + "A line drawn straight across missing periods", + "Changing tick increments to hide missing data" + ], + "measure": { + "kind": "count", + "what": "For each bar, column or area chart (SVG or canvas with an accessible data table or chart library config), count those whose value-axis minimum is not 0 for all-positive data; for each time-series line, count runs where a missing period is bridged by a continuous segment.", + "mustFail": "A bar chart of monthly revenue whose y-axis runs from 9,000 to 10,000, making a 3% difference look like a 5× one.", + "mustPass": "The same bars on a 0–10,000 axis; a line chart of the same data cropped to 9,000–10,000 with a visible gap for the month with no data." + }, + "exceptions": [ + "Line charts and scatter plots, where Carbon allows a non-zero start because the trend matters more than relative size", + "Log-scale charts, which have no zero and must say they are log scale" + ], + "kinds": { + "marketing": "maybe", + "content": "maybe", + "data": "yes", + "workspace": "maybe", + "shop": "no", + "transaction": "no" + }, + "kindsWhy": "Applies to any page that shows a chart; data dashboards most, marketing and content pages when they show figures, shop and transaction pages almost never.", + "agrees": [ + "canon.tufte-graphical-integrity" + ] + }, + { + "id": "data-display.chart-axes-and-bars-are-labelled", + "claim": "Quantitative charts label both axes outside the data area in short plain words, and label each bar with what it shows and its value; past about six bars, use a table.", + "source": { + "author": "Shopify Polaris", + "work": "Data visualizations — Axis and labelling conventions; Horizontal bar charts", + "url": "https://polaris-react.shopify.com/design/data-visualizations", + "quote": "All standard charts that show quantitative data have 2 axes that should be labeled for clarity. … Label each bar with what it’s displaying, as well as the value." + }, + "evidence": "author", + "prefers": [ + "Axis titles naming the measure and its unit", + "Direct labels on or beside each bar, outside the bar when it is too short", + "Tick labels skipped at regular intervals rather than crammed or slanted", + "Short standard abbreviations: 'Feb', '10 Apr', '1.2k'", + "A legend only when there is more than one series (Primer)" + ], + "forbids": [ + "Unlabelled axes or units only in a tooltip", + "Slanted tick labels squeezed in to fit", + "A bar chart of dozens of categories where a table would compare them better" + ], + "measure": { + "kind": "count", + "what": "For each chart, count value or category axes with no visible title or unit text, bars or points with no visible label or value (where there are six or fewer), tick labels rotated away from horizontal, and horizontal bar charts with more than six bars.", + "mustFail": "A bar chart of sales by product with no axis titles, values only on hover, and 14 bars with tick labels rotated 45°.", + "mustPass": "Six bars each labelled 'T-shirts · $4.2k', an x-axis titled 'Sales (USD)', and the remaining products in a sortable table below." + }, + "exceptions": [ + "Sparklines and small multiples that share labelled axes with a neighbour", + "Interactive charts where exact values sit in a tooltip, provided the axes still carry titles and units" + ], + "kinds": { + "marketing": "maybe", + "content": "maybe", + "data": "yes", + "workspace": "maybe", + "shop": "no", + "transaction": "no" + }, + "kindsWhy": "Applies wherever a chart is drawn; shop and transaction flows almost never draw one.", + "agrees": [ + "canon.tufte-data-ink-ratio", + "canon.tufte-forgo-chartjunk" + ] + } + ] +} diff --git a/skills/uxcli/lenses/viewpoints/forms.json b/skills/uxcli/lenses/viewpoints/forms.json new file mode 100644 index 0000000..84ed69c --- /dev/null +++ b/skills/uxcli/lenses/viewpoints/forms.json @@ -0,0 +1,528 @@ +{ + "schema_version": 1, + "school": "forms", + "name": "Forms and input", + "research": "docs/lenses/forms.md", + "viewpoints": [ + { + "id": "forms.error-summary-at-the-top", + "claim": "After a failed submit, show an error summary at the top of the page that takes focus and links each error to its field — even when there is only one error.", + "source": { + "author": "GOV.UK Design System", + "work": "Error summary component", + "url": "https://design-system.service.gov.uk/components/error-summary/", + "quote": "Always show an error summary when there is a validation error, even if there’s only one." + }, + "evidence": "author", + "prefers": [ + "A summary above the h1 (below any back link) with a heading such as 'There is a problem'", + "Move keyboard focus to the summary when it appears", + "One link per error, pointing at the field (or the first field of a date or radio group)", + "Word each summary item exactly like the message beside its field", + "Prefix the page <title> with 'Error: '" + ], + "forbids": [ + "Errors shown only beside fields far down a long form, with focus left on the submit button", + "A summary of plain text with no links to the fields", + "A toast or banner that disappears before it can be read" + ], + "measure": { + "kind": "count", + "what": "Submit the form empty. Within 1 s: is there an element above the first h1 of main that has focus (document.activeElement inside it), contains one link per invalid field, and does each link's href resolve to the id of an invalid input? Does document.title start with 'Error'?", + "mustFail": "A 6-field form submitted empty paints red text under each field and a red border; focus stays on Submit; no summary at top.", + "mustPass": "Same form shows 'There is a problem' above the h1 with 6 links to #field ids, focused, and the title reads 'Error: Apply for a licence'." + }, + "exceptions": [ + "Primer suggests the interactive summary only for 3 or more errors, and otherwise focusing the first invalid field", + "A one-field form (search, single email sign-up) can rely on the message beside the field" + ], + "kinds": { + "marketing": "maybe", + "content": "no", + "data": "maybe", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Applies wherever a form can fail validation; marketing sign-ups are often one field, and content pages rarely carry forms.", + "agrees": [ + "modern.feedback-is-local-and-optimistic" + ] + }, + { + "id": "forms.error-message-says-how-to-fix", + "claim": "Each error message beside a field says what went wrong and how to fix it, in the words of the field's label — never 'Invalid' or 'This field is required'.", + "source": { + "author": "GOV.UK Design System", + "work": "Error message component — Be clear and concise", + "url": "https://design-system.service.gov.uk/components/error-message/", + "quote": "Describe what has happened and tell them how to fix it. The message must be in plain English, use positive language and get to the point." + }, + "evidence": "author", + "prefers": [ + "Specific messages per failure: empty, too long, wrong format, out of range", + "Reuse the label's words: 'Enter how many hours you work a week'", + "Instructions for empty fields ('Enter your first name'), descriptions for limits ('Name must be 35 characters or less')", + "Message placed after the label and hint, tied to the input with aria-describedby" + ], + "forbids": [ + "'Invalid', 'valid', 'An error occurred', 'This field is required', 'Fill in the field'", + "Jargon or codes such as 'form post error' or 'error 0x0000000643'", + "'please', 'sorry', 'oops', 'you forgot'" + ], + "measure": { + "kind": "count", + "what": "Submit with each field empty and then with a wrong-format value. For each error message: does its text avoid the generic list (invalid, required, error occurred, please, sorry, oops), share at least one content word with its field's label, and is it referenced by the input's aria-describedby?", + "mustFail": "Email field left empty shows 'This field is required'; filled with 'bob' shows 'Invalid input'.", + "mustPass": "Empty shows 'Enter your email address'; 'bob' shows 'Enter an email address in the correct format, like name@example.com'." + }, + "exceptions": [ + "Do not repeat an example in the message when the hint already shows one" + ], + "kinds": { + "marketing": "maybe", + "content": "no", + "data": "maybe", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Any page that validates input; content pages rarely do.", + "agrees": [ + "usability.speak-the-users-language" + ] + }, + { + "id": "forms.validate-when-the-user-is-done", + "claim": "Do not show an error while the user is still typing; validate when they try to continue, and only add earlier validation where research shows it helps.", + "source": { + "author": "GOV.UK Design System", + "work": "Recover from validation errors pattern — When to tell the user about validation errors", + "url": "https://design-system.service.gov.uk/patterns/validation/", + "quote": "Generally speaking, avoid validating the information in a field before the user has finished entering it. This sort of validation can cause problems - especially for users who type more slowly." + }, + "evidence": "author", + "prefers": [ + "Validate on Continue or Submit", + "After a failed submit, update a field's error live once the user fixes it (Primer)", + "A character count is the accepted exception: warn as the limit is passed" + ], + "forbids": [ + "An error that appears on the first keystroke of an email or phone field", + "Red borders on an untouched form at load", + "Browser-native HTML5 validation bubbles in place of designed messages" + ], + "measure": { + "kind": "count", + "what": "Focus an email or formatted field and type one character, keeping focus. Wait 1 s. Does any error message, aria-invalid=true or error colour appear before blur or submit? Also load the form fresh: are any fields already marked invalid?", + "mustFail": "Typing 'a' into Email immediately shows 'Enter a valid email' in red under the field.", + "mustPass": "Typing 'a' shows nothing; pressing Continue with 'a' shows the field error and the summary." + }, + "exceptions": [ + "GOV.UK goes further and says not to validate on blur either; Baymard (usability.inline-validation-after-leaving-field) recommends blur validation for hard fields — the schools agree only on 'not while typing'", + "Primer allows validating as the user types once the field has already been flagged invalid, so the error clears as soon as it is fixed" + ], + "kinds": { + "marketing": "maybe", + "content": "no", + "data": "maybe", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Applies to any form with validation; content pages rarely have one.", + "agrees": [] + }, + { + "id": "forms.keep-answers-after-an-error", + "claim": "When a submit fails, show the form again with every answer the user gave still in it — the failing ones and the passing ones.", + "source": { + "author": "GOV.UK Design System", + "work": "Error message component — How it works", + "url": "https://design-system.service.gov.uk/components/error-message/", + "quote": "Do not clear any form fields when showing the Error message component. Keep both passing and failing answers." + }, + "evidence": "author", + "prefers": [ + "Re-render server-side errors with submitted values filled in", + "Keep the failing value so the user can see and edit what went wrong", + "Pre-populate fields when the user goes back to change an answer" + ], + "forbids": [ + "A reload that empties the form after a server error", + "Clearing the field that failed", + "Clearing password-adjacent fields such as name and email along with the password" + ], + "measure": { + "kind": "count", + "what": "Fill every field with valid values except one, submit, and after the error page paints read each input's value: how many fields that had a value now read empty?", + "mustFail": "A 7-field registration form with a bad postcode returns with all 7 fields empty.", + "mustPass": "The same form returns with all 7 values in place, the postcode field marked in error." + }, + "exceptions": [ + "Password and card security code fields may be cleared for security, and the message should say so" + ], + "kinds": { + "marketing": "maybe", + "content": "no", + "data": "maybe", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Wherever a submit can fail; content pages rarely have forms.", + "agrees": [] + }, + { + "id": "forms.mark-optional-fields-in-words", + "claim": "Ask mostly required questions and label the exceptions '(optional)' in words; do not mark required fields with asterisks.", + "source": { + "author": "GOV.UK Design System", + "work": "Question pages pattern", + "url": "https://design-system.service.gov.uk/patterns/question-pages/", + "quote": "in most contexts, add ‘(optional)’ to the labels of optional fields" + }, + "evidence": "author", + "prefers": [ + "'(optional)' in the label, or in the legend for a radio or checkbox group", + "Removing optional questions before marking them", + "One convention across the whole service" + ], + "forbids": [ + "Asterisks on mandatory fields", + "Optional fields that look identical to required ones", + "Required and optional marked differently on different pages" + ], + "measure": { + "kind": "count", + "what": "Submit the form with every field empty and note which fields raise no error (optional). Count optional fields whose label or legend text lacks 'optional', and labels that contain '*'.", + "mustFail": "A 6-field form marks 4 required fields with a red '*' and leaves the 2 optional fields unmarked.", + "mustPass": "The same form has no asterisks; the 2 optional fields read 'Phone number (optional)' and 'Company (optional)'." + }, + "exceptions": [ + "USWDS instead marks required fields with a red asterisk explained by a note at the top, and also labels optional fields '(optional)' — the schools agree that optional fields say so in words", + "Primer and USWDS exempt one-field forms and login forms, where every field is plainly required" + ], + "kinds": { + "marketing": "maybe", + "content": "no", + "data": "maybe", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Any multi-field form; content pages rarely have one.", + "agrees": [ + "usability.fewer-checkout-fields" + ] + }, + { + "id": "forms.autocomplete-names-the-purpose", + "claim": "Every field that asks about the user — name, email, phone, address, postcode, birthday, card — carries the matching autocomplete token so browsers and assistive tech can fill and label it.", + "source": { + "author": "W3C Web Accessibility Initiative", + "work": "Understanding WCAG 2.2 Success Criterion 1.3.5: Identify Input Purpose", + "url": "https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html", + "quote": "Use code to indicate the purpose of common inputs, where technology allows." + }, + "evidence": "author", + "prefers": [ + "autocomplete='name' or 'given-name' and 'family-name' on name fields", + "'email', 'tel', 'postal-code', 'street-address' or 'address-line1', 'bday-day'/'bday-month'/'bday-year'", + "'shipping' and 'billing' section tokens on order forms", + "autocomplete='off' on the form, if needed, while each field still declares its purpose" + ], + "forbids": [ + "autocomplete='off' on personal fields with no purpose token", + "Personal-data fields with no autocomplete attribute", + "A token that does not match the field (email token on a phone field)" + ], + "measure": { + "kind": "count", + "what": "For every input, select and textarea whose label matches name, email, phone, address, postcode/ZIP, city, country, date of birth, card number or expiry: count those whose autocomplete attribute is missing, 'off', or not a WCAG input-purpose token that matches the label.", + "mustFail": "A checkout with Name, Email, Phone, Address line 1, Postcode inputs, none with autocomplete.", + "mustPass": "The same inputs carry name, email, tel, address-line1, postal-code." + }, + "exceptions": [ + "Fields about someone other than the user (a recipient's email) are outside 1.3.5", + "A field that accepts either username or email may carry one token or none" + ], + "kinds": { + "marketing": "yes", + "content": "maybe", + "data": "no", + "workspace": "maybe", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Sign-up, checkout and application forms ask about the user; data and workspace forms mostly ask about records, not the person.", + "agrees": [] + }, + { + "id": "forms.input-type-matches-the-answer", + "claim": "Give each field the type and on-screen keyboard its answer needs — email, tel, inputmode numeric or decimal for numbers — and avoid type=number.", + "source": { + "author": "GOV.UK Design System", + "work": "Text input component — Numbers", + "url": "https://design-system.service.gov.uk/components/text-input/", + "quote": "With <input type=\"number\"> there’s a risk of users accidentally incrementing a number when they’re trying to do something else - for example, scroll up or down the page." + }, + "evidence": "author", + "prefers": [ + "type='email' with spellcheck=false for email addresses", + "type='tel' for phone numbers", + "type='text' with inputmode='numeric' for whole numbers and codes, inputmode='decimal' for amounts", + "spellcheck=false on names, references and codes" + ], + "forbids": [ + "type='number' for card numbers, postcodes, account numbers or reference codes", + "type='text' with no inputmode on a field that only accepts digits", + "Blocking paste into any field" + ], + "measure": { + "kind": "count", + "what": "For each input, read type and inputmode against its label: count email fields not type=email, phone fields not type=tel, digit-only fields (amount, quantity, code, card, account) with neither inputmode numeric/decimal nor type=tel, and any type=number.", + "mustFail": "Phone is type=text, card number is type=number, quantity is type=text with no inputmode.", + "mustPass": "Phone is type=tel, card number is type=text inputmode=numeric autocomplete=cc-number, quantity is inputmode=numeric." + }, + "exceptions": [ + "type=number when research shows a need, such as a stepper on a small bounded quantity", + "Negative amounts: GOV.UK advises type=text without inputmode, since some keypads have no minus key" + ], + "kinds": { + "marketing": "maybe", + "content": "no", + "data": "maybe", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Wherever users type structured values; content pages rarely ask.", + "agrees": [ + "modern.mobile-inputs-do-not-zoom-or-trap" + ] + }, + { + "id": "forms.memorable-dates-as-three-fields", + "claim": "Ask for a date the user already knows (birth, issue date) as separate day, month and year fields in a fieldset; keep calendar pickers for choosing near dates, and never as the only way in.", + "source": { + "author": "GOV.UK Design System", + "work": "Date input component", + "url": "https://design-system.service.gov.uk/components/date-input/", + "quote": "Use the date input component when you’re asking users for a date they’ll already know, or can look up without using a calendar." + }, + "evidence": "author", + "prefers": [ + "Three labelled inputs — Day, Month, Year — inside a fieldset whose legend is the question", + "A hint example with day ≥ 13 and month ≤ 9, e.g. '27 3 2007'", + "Accept month names ('jan', 'January')", + "bday-day, bday-month, bday-year autocomplete for date of birth" + ], + "forbids": [ + "A calendar popup as the only way to enter a date of birth", + "Three dropdowns for day, month and year", + "Auto-advancing focus from day to month to year", + "A single free-text date field with no stated format" + ], + "measure": { + "kind": "count", + "what": "Find each date question. For a past or memorable date: is it three text inputs inside one fieldset with a legend, each with a visible label? Type two digits into Day: does focus stay in Day? Does a text path exist when a calendar control is present?", + "mustFail": "Date of birth is a single input that opens a JS calendar starting on today's month; typing is blocked.", + "mustPass": "Date of birth is Day/Month/Year inputs in a fieldset legend 'What is your date of birth?', hint 'For example, 27 3 2007', focus does not jump." + }, + "exceptions": [ + "Dates copied from a document or card match that document's format (MM/YY for card expiry)", + "Booking a date in the near future, or where the weekday matters, suits a calendar — with a text input as well", + "Approximate dates ('December 2017') may drop the day field", + "The ban on three dropdowns comes from craft.anything-but-dropdowns, not from this source." + ], + "kinds": { + "marketing": "no", + "content": "no", + "data": "maybe", + "workspace": "maybe", + "shop": "maybe", + "transaction": "yes" + }, + "kindsWhy": "Memorable dates are asked in applications and registrations; data filters and bookings are near-date picks that suit a calendar.", + "agrees": [ + "craft.anything-but-dropdowns" + ] + }, + { + "id": "forms.ask-only-what-you-need", + "claim": "Every question on the form has a named use; drop the ones nobody can justify, and never ask for the same thing twice in one journey.", + "source": { + "author": "GOV.UK Design System", + "work": "Question pages pattern", + "url": "https://design-system.service.gov.uk/patterns/question-pages/", + "quote": "You should make sure you know why you’re asking every question and only ask users for information you really need." + }, + "evidence": "author", + "prefers": [ + "A question protocol: for each field, who uses the answer and what for", + "Reuse an earlier answer by pre-filling it rather than asking again", + "Let 'I do not know' be an answer where it is a valid one", + "Say why a sensitive question is asked, in its hint" + ], + "forbids": [ + "'Nice to have' fields such as title, gender or 'how did you hear about us' with no stated use", + "Asking for an email or address a second time in the same journey", + "Confirm-email fields that make the user type the address twice, instead of playing it back for checking" + ], + "measure": { + "kind": "question", + "what": "List every field on the form. For each, can the team name who reads the answer and what decision it changes? How many fields have no answer, and how many repeat information given earlier in the journey?" + }, + "exceptions": [ + "Legal or regulatory questions whose use is mandated, which should still say why in a hint" + ], + "kinds": { + "marketing": "yes", + "content": "no", + "data": "maybe", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Sign-up, checkout and application forms all pay for each extra field; content pages rarely ask anything.", + "agrees": [ + "usability.fewer-checkout-fields", + "usability.minimalist-no-competing-information" + ] + }, + { + "id": "forms.group-related-inputs-in-a-fieldset", + "claim": "Wrap each set of inputs that answer one question — radios, checkboxes, a date, an address — in a fieldset whose first child is a legend naming the question.", + "source": { + "author": "U.S. Web Design System (USWDS)", + "work": "Form component — Accessibility guidance", + "url": "https://designsystem.digital.gov/components/form/", + "quote": "Group each set of thematically related controls in a fieldset element. Use the legend element to offer a label within each one." + }, + "evidence": "author", + "prefers": [ + "fieldset + legend for every radio group and checkbox group", + "fieldset + legend for date (day/month/year) and multi-line address", + "The legend as the page h1 when the page asks only that question", + "'(optional)' in the legend when the whole group is optional" + ], + "forbids": [ + "A radio group whose question is a <p> or <div> above the radios", + "Radio and checkbox groups with no group name for assistive tech", + "A fieldset with no legend or an empty one" + ], + "measure": { + "kind": "count", + "what": "Count radio groups (same name), checkbox groups, and day/month/year triples. How many are not inside a fieldset whose first element child is a non-empty legend (or a role=group / radiogroup with aria-labelledby pointing at visible text)?", + "mustFail": "'How would you like to be contacted?' is a bold <p>; three radios follow with no fieldset.", + "mustPass": "The three radios sit in <fieldset><legend>How would you like to be contacted?</legend>…</fieldset>." + }, + "exceptions": [ + "A single checkbox (I agree) needs no fieldset", + "Native fieldset/legend can be replaced by role=group with aria-labelledby where styling requires it" + ], + "kinds": { + "marketing": "maybe", + "content": "no", + "data": "maybe", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Any form with choice groups or composite answers; content pages rarely have one.", + "agrees": [ + "canon.gestalt-common-region" + ] + }, + { + "id": "forms.hint-text-is-short-and-linked", + "claim": "Hint text sits between the label and the input, is one short sentence with no links, and is tied to the input with aria-describedby; longer explanation goes in the page body.", + "source": { + "author": "GOV.UK Design System", + "work": "Text input component — Hint text", + "url": "https://design-system.service.gov.uk/components/text-input/", + "quote": "Keep hint text to a single short sentence, without any full stops." + }, + "evidence": "author", + "prefers": [ + "Hint placed after the label and before the input", + "The input's aria-describedby lists the hint's id (and the error's, when shown)", + "Hints that say where to find the answer or how it will be used", + "A statement heading plus body text when a question needs a long explanation" + ], + "forbids": [ + "Links inside hint text", + "Multi-sentence paragraphs as hints", + "Hints placed below the input where they are read after typing", + "Hints in a tooltip behind an icon" + ], + "measure": { + "kind": "count", + "what": "For every element used as a hint (referenced by an input's aria-describedby, or styled as hint between label and input): count hints containing an <a>, hints longer than one sentence or about 120 characters, hints rendered below their input, and visible hints not referenced by their input's aria-describedby.", + "mustFail": "'National Insurance number' input has, below it, a 3-sentence grey paragraph with a link 'Find your number', not referenced by aria-describedby.", + "mustPass": "Between label and input: 'It’s on your National Insurance card, benefit letter, payslip or P60' with id referenced by the input's aria-describedby." + }, + "exceptions": [ + "A format example ('For example, QQ 12 34 56 C') may follow the sentence", + "\"About 120 characters\" in \"look at\" is uxcli's reading of \"a single short sentence\", not a number the source gives." + ], + "kinds": { + "marketing": "maybe", + "content": "no", + "data": "maybe", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Wherever fields need help; content pages rarely have forms.", + "agrees": [ + "usability.help-in-context", + "usability.placeholders-are-not-labels" + ] + }, + { + "id": "forms.do-not-disable-the-submit-button", + "claim": "Keep the submit button enabled while the form is incomplete; let a press show what is missing instead of greying the button out.", + "source": { + "author": "GitHub Primer", + "work": "Forms UI pattern — Validation", + "url": "https://primer.style/product/ui-patterns/forms/overview/", + "quote": "Disabled buttons are discouraged, as they don't clearly communicate what actions a user should take to complete a form." + }, + "evidence": "author", + "prefers": [ + "An always-enabled submit that runs validation and shows the error summary", + "aria-disabled with an explanation where a control truly must be unavailable", + "Ignoring a second click while a submit is in flight, with a visible busy state" + ], + "forbids": [ + "A grey, unfocusable submit until every field validates", + "A disabled button with no text saying why", + "Disabled text inputs used as read-only display" + ], + "measure": { + "kind": "count", + "what": "Load the form untouched. Is the primary submit button disabled (disabled attribute, aria-disabled=true, or pointer-events:none)? If enabled, does pressing it produce visible field errors within 1 s?", + "mustFail": "'Create account' is disabled and grey until all 5 fields are valid; clicking it does nothing.", + "mustPass": "'Create account' is enabled; pressing it on an empty form shows the error summary and 5 field messages." + }, + "exceptions": [ + "Briefly blocking a second submit while the first is processing (USWDS, GOV.UK prevent-double-click)", + "GOV.UK: only when research shows a disabled button makes the interface easier to understand" + ], + "kinds": { + "marketing": "maybe", + "content": "no", + "data": "maybe", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Any form with a submit; content pages rarely have one.", + "agrees": [ + "usability.visibility-of-system-status" + ] + } + ] +} diff --git a/skills/uxcli/lenses/viewpoints/writing.json b/skills/uxcli/lenses/viewpoints/writing.json new file mode 100644 index 0000000..d636bb1 --- /dev/null +++ b/skills/uxcli/lenses/viewpoints/writing.json @@ -0,0 +1,453 @@ +{ + "schema_version": 1, + "school": "writing", + "name": "Writing and microcopy", + "research": "docs/lenses/writing.md", + "viewpoints": [ + { + "id": "writing.errors-say-what-and-how-to-fix", + "claim": "An error message says what went wrong and how to fix it, in plain words, using the field's own label, without blame, apology or generic filler.", + "source": { + "author": "GOV.UK Design System (Government Digital Service)", + "work": "Error message component, 'Be clear and concise'", + "url": "https://design-system.service.gov.uk/components/error-message/", + "quote": "Describe what has happened and tell them how to fix it. The message must be in plain English, use positive language and get to the point." + }, + "evidence": "author", + "prefers": [ + "Reuse the words of the field's label in its error ('Enter how many hours you work a week')", + "Write a different message for each failure: empty, too long, too short, wrong format", + "Use an instruction for empty fields and a description for out-of-range values, consistently", + "Show the same text next to the field and in the error summary", + "Keep the user's answer in the field when showing the error" + ], + "forbids": [ + "Generic messages: 'An error occurred', 'This field is required', 'Select an option', 'Fill in the field'", + "Technical jargon and codes: 'form post error', 'error 0x0000000643'", + "Blame or legalese: 'forbidden', 'illegal', 'you forgot', 'prohibited'", + "'Invalid' / 'valid', 'please', 'sorry' and 'oops'", + "Using an inline field error to say the user is ineligible or the service is down" + ], + "measure": { + "kind": "count", + "what": "Submit each form empty and with a malformed value; collect every rendered error text (role=alert, aria-invalid fields' aria-describedby, .error classes). Count messages that (a) match a generic list ('an error occurred', 'this field is required', 'invalid', 'error', 'required', 'something went wrong' with no next step), (b) contain 'oops', 'sorry', 'please', 'illegal', 'forbidden', 'you forgot', or a bare code, or (c) share no content word with their field's label. Count must be 0.", + "mustFail": "An email field labelled 'Email address' that, left empty, shows 'This field is required' and, malformed, shows 'Invalid input'.", + "mustPass": "Same field shows 'Enter your email address' when empty and 'Enter an email address in the correct format, like name@example.com' when malformed." + }, + "exceptions": [ + "Problems the user cannot fix (eligibility, permissions, outages) belong on a page that explains what to do next, not in a field error", + "Do not repeat an example in the error if the same example is already shown as hint text" + ], + "kinds": { + "marketing": "maybe", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Marketing and content pages usually have at most a sign-up or search form; anything with real input needs this everywhere.", + "agrees": [ + "forms.error-message-says-how-to-fix", + "modern.feedback-is-local-and-optimistic", + "usability.speak-the-users-language" + ] + }, + { + "id": "writing.buttons-name-the-action", + "claim": "A button's label is a verb (plus its object when needed) that says what happens when it is pressed; vague labels like OK, Done, Submit or Yes are replaced whenever a specific action exists.", + "source": { + "author": "IBM Carbon Design System", + "work": "Dialog pattern, anatomy: Actions", + "url": "https://carbondesignsystem.com/patterns/dialog-pattern/", + "quote": "Use descriptive words for the actions like Add, Delete, Save and avoid vague words like Done or OK." + }, + "evidence": "author", + "prefers": [ + "Start the label with a verb: 'Save and continue', 'Send invoice', 'Add product'", + "Name the object when the verb alone is ambiguous on that screen", + "Make the dialog title and its confirming button use the same verb" + ], + "forbids": [ + "'OK', 'Done', 'Submit', 'Yes', 'Go', 'Confirm' where a specific action exists", + "Labels that describe the button rather than the action ('Button', 'Click')", + "Icon-only buttons without an accessible name" + ], + "measure": { + "kind": "count", + "what": "List every visible button and input[type=submit] with its accessible name. Count those whose trimmed, lower-cased name is in the generic set {ok, okay, done, submit, yes, go, confirm, click here, click, button} or is empty. Count must be 0 outside of a platform-standard alert with a single acknowledgement.", + "mustFail": "A 'Delete project?' dialog whose buttons read 'OK' and 'Cancel', and a sign-up form whose button reads 'Submit'.", + "mustPass": "The dialog's buttons read 'Delete project' and 'Cancel'; the form's button reads 'Create account'." + }, + "exceptions": [ + "A purely informational alert with one acknowledgement button may say 'OK'", + "GOV.UK uses 'Continue' for a question page that does not save the user's answers — a convention for multi-page forms, not a generic label" + ], + "kinds": { + "marketing": "yes", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Content pages often have few buttons beyond navigation; every other kind leads with actions.", + "agrees": [ + "modern.no-junk-drawer-menus-or-unlabeled-icons" + ] + }, + { + "id": "writing.links-describe-their-destination", + "claim": "Link text names where the link goes or what it opens, read on its own; never 'click here', 'here', 'read this' or 'more'.", + "source": { + "author": "Mailchimp", + "work": "Content Style Guide, Web Elements: Links", + "url": "https://styleguide.mailchimp.com/web-elements/", + "quote": "Don’t say things like “Click here!” or “Click for more information” or “Read this.” Write the sentence as you normally would, and link relevant keywords." + }, + "evidence": "author", + "prefers": [ + "Link the keywords that name the destination ('Read the automation guide')", + "Keep trailing punctuation outside the link", + "At most one 'Learn more' per screen, and only when the sentence already names the topic" + ], + "forbids": [ + "'Click here', 'here', 'this', 'read this', 'more', 'link' as the whole link text", + "Several links with the same text pointing to different places" + ], + "measure": { + "kind": "count", + "what": "List every visible <a href> with its accessible name. Count links whose name, trimmed and lower-cased without punctuation, is in {click here, here, this, read this, more, read more, learn more, link, click} AND has no aria-label/aria-describedby adding the topic; also count sets of links with identical names but different hrefs. Count must be 0 (allow one 'learn more' per page when it follows a sentence naming the topic).", + "mustFail": "'To set up payments, click [here].' and three cards each ending in a 'Read more' link to different articles.", + "mustPass": "'Set up [payments] before your first sale.' and cards whose titles are the links." + }, + "exceptions": [ + "A single 'Learn more' after a sentence that names the topic, as Polaris allows" + ], + "kinds": { + "marketing": "yes", + "content": "yes", + "data": "maybe", + "workspace": "maybe", + "shop": "yes", + "transaction": "maybe" + }, + "kindsWhy": "Data, workspace and transaction screens are built mostly from buttons and controls; prose links matter most where people read.", + "agrees": [ + "usability.mindless-clicks-not-fewer-clicks" + ] + }, + { + "id": "writing.one-label-per-action", + "claim": "One concept, one word: controls that do the same thing carry the same label everywhere, and controls that do different things never share a label.", + "source": { + "author": "GitHub Primer", + "work": "Accessibility guide: Descriptive buttons, 'How to test names'", + "url": "https://primer.style/guides/accessibility/descriptive-buttons", + "quote": "When buttons perform the same action, they have the same name." + }, + "evidence": "author", + "prefers": [ + "Keep a terminology list of preferred words and words not to use for the product", + "Pick one verb per action (Delete or Remove, not both for the same thing) and reuse it across pages", + "Add the object to disambiguate repeated actions ('Remove Apples', 'Remove Pears')" + ], + "forbids": [ + "Synonyms for one action across screens: 'Save' here, 'Update' there, 'Apply' elsewhere", + "Identical labels for different actions on the same page", + "Naming the same object two ways ('workspace' and 'project') in one product" + ], + "measure": { + "kind": "count", + "what": "Across the journey's pages, collect (accessible name, action) pairs for buttons and links, where action is the form action/href/handler target. Count names that map to two or more different actions on one page, and actions reached by two or more different names across pages; also flag known synonym pairs present together (save/update/apply, delete/remove, sign in/log in, cart/basket). Count must be 0.", + "mustFail": "A list where each row has a 'Remove' button with no accessible name difference, and settings pages using 'Save' on one tab and 'Update' on another for the same submit.", + "mustPass": "Row buttons named 'Remove Apples', 'Remove Pears'; every settings tab submits with 'Save changes'." + }, + "exceptions": [ + "Delete and Remove may coexist when they mean different things (destroy vs take out of a collection), as Carbon defines them — then each must be used only for its own meaning" + ], + "kinds": { + "marketing": "maybe", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "A one-page marketing or content site has few repeated actions; consistency matters wherever people move between screens doing tasks.", + "agrees": [] + }, + { + "id": "writing.sentence-case-ui-text", + "claim": "Labels, buttons, headings, tabs and table headers are written in sentence case; capitals only for the first word, proper nouns and acronyms — never title case or all caps for emphasis.", + "source": { + "author": "IBM Carbon Design System", + "work": "Content guidelines: Writing style, Capitalization", + "url": "https://carbondesignsystem.com/guidelines/content/writing-style/", + "quote": "Use sentence–case capitalization for all UI text elements." + }, + "evidence": "author", + "prefers": [ + "'First name', 'Email address', 'Save changes'", + "Refer to a UI element with the capitalization it has in the UI ('the My network page')", + "Use bold or italic, not capitals, for emphasis" + ], + "forbids": [ + "Title Case Buttons Like 'Save Your Changes'", + "ALL CAPS text set in the markup (as opposed to a tracked small label styled with text-transform)", + "Capitalizing feature names to mark them as special" + ], + "measure": { + "kind": "count", + "what": "For each button, label, th, tab and h1–h6 with three or more words, take the rendered text (after text-transform) and count those where two or more non-initial words that are not acronyms or in the page's proper-noun list start with a capital (title case), or where the whole text of four or more words is upper case. Count must be 0.", + "mustFail": "A button 'Save Your Changes', a heading 'Manage Team Members', and a table header 'ORDER DATE PLACED'.", + "mustPass": "'Save your changes', 'Manage team members', 'Order date placed'." + }, + "exceptions": [ + "Proper nouns, product names and acronyms keep their capitals", + "Mailchimp's own guide uses title case for page titles, menu names and global navigation — a house style, decide one and keep it", + "Short tracked overline labels in caps are a typographic choice; the four-word floor leaves them alone" + ], + "kinds": { + "marketing": "yes", + "content": "yes", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Every kind of UI has labels and buttons.", + "agrees": [ + "usability.omit-needless-words" + ] + }, + { + "id": "writing.plain-language-reading-level", + "claim": "Interface text uses plain, everyday words and short sentences, at about a 7th-grade reading level, with sentences that start with the verb when they ask for action.", + "source": { + "author": "Shopify Polaris", + "work": "Content: Fundamentals, 'Write like merchants talk'", + "url": "https://polaris.shopify.com/content/fundamentals", + "quote": "Aim for a 7th grade reading level—it’s easiest for merchants to digest" + }, + "evidence": "author", + "prefers": [ + "Short, common words: 'use' not 'utilize', 'large' not 'voluminous'", + "Direct instructions starting with a verb ('Add apps', not 'You can add apps')", + "Contractions where they read naturally", + "Keep jargon only when it is what the actual users say" + ], + "forbids": [ + "Long multi-clause sentences in UI text", + "Inflated words chosen to sound impressive", + "Passive constructions that hide who does what" + ], + "measure": { + "kind": "count", + "what": "Extract visible running text (p, li, label hint text, dialog and banner bodies; skip user-generated content and legal text). Compute Flesch–Kincaid grade per block of 2+ sentences and the word count per sentence. Count blocks above grade 9 and sentences over 25 words. Count must be 0.", + "mustFail": "A hint reading 'In order to facilitate the expeditious processing of your application, it is imperative that all requisite documentation be furnished prior to submission.'", + "mustPass": "'Upload your documents before you send your application. We need them to process it quickly.'" + }, + "exceptions": [ + "Expert tools whose users speak a domain language — readability formulas punish necessary domain terms; judge those against the journey's actor", + "Legal terms that must be quoted verbatim", + "The numeric thresholds in \"look at\" are uxcli's starting point, not the source's; the source states the rule, not the number." + ], + "kinds": { + "marketing": "yes", + "content": "maybe", + "data": "maybe", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Long-form content and data labels may be written for specialists; the grade target fits task and selling copy best.", + "agrees": [ + "usability.speak-the-users-language", + "usability.omit-needless-words" + ] + }, + { + "id": "writing.empty-states-say-what-next", + "claim": "An empty state's copy says what will appear in this space and how to start, with one action that begins it; if it is empty because of an error, it says what went wrong and how to recover.", + "source": { + "author": "GitHub Primer", + "work": "UI patterns: Empty states, Secondary text", + "url": "https://primer.style/ui-patterns/empty-states", + "quote": "From the text, users should be able to understand what steps they might take next." + }, + "evidence": "author", + "prefers": [ + "A title that names the purpose of the space ('No repositories yet')", + "One sentence on what the feature is for", + "One primary action that starts the creation flow, named for it ('Create repository')", + "For empty search results, say how to change the search or filters" + ], + "forbids": [ + "A bare 'No data', 'No results' or 'Nothing here' with no next step", + "Vague error text like 'There was a problem'", + "Over-literal infrastructure detail ('The US East-2 database cluster is down')" + ], + "measure": { + "kind": "count", + "what": "Render each list, table, search and dashboard with zero items (the journey's empty state). For each empty container, count it when the visible text is under 6 words or matches {no data, no results, nothing here, empty, no items} AND there is no button or link inside or adjacent to it. Count must be 0.", + "mustFail": "An invoices table that, with no invoices, shows only the header row and 'No data'.", + "mustPass": "'No invoices yet. Invoices you send to customers appear here.' with a 'Create invoice' button." + }, + "exceptions": [ + "Temporarily empty feeds (no notifications) need a message but not an action", + "Empty states caused by missing permissions point to who can grant access rather than a creation action", + "The numeric thresholds in \"look at\" are uxcli's starting point, not the source's; the source states the rule, not the number." + ], + "kinds": { + "marketing": "no", + "content": "maybe", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "maybe" + }, + "kindsWhy": "Marketing pages ship with their content; search results, carts, dashboards and workspaces start or end empty.", + "agrees": [ + "craft.dont-overlook-empty-states-teach-by-example", + "modern.every-state-is-designed" + ] + }, + { + "id": "writing.success-names-what-happened", + "claim": "A success message names the thing and what happened to it, in a few words ('Product updated'), instead of a generic 'Success!' or a wordy 'Your product has been successfully updated'.", + "source": { + "author": "Shopify Polaris", + "work": "Toast component, Content guidelines: Message", + "url": "https://polaris.shopify.com/components/internal-only/toast", + "quote": "Written in the pattern of: noun + verb" + }, + "evidence": "author", + "prefers": [ + "Object + past-tense verb: 'Collection added', 'Invoice sent'", + "Include the identifier when there are several (e.g. 'Issue #21 created' with a link)", + "On the final page of a transaction, add a reference and what happens next" + ], + "forbids": [ + "A bare 'Success!', 'Done' or 'Saved successfully' with no object", + "'Your X has been successfully Y-ed' padding", + "Title Case or field-colon formats like 'Discount: Saved successfully'" + ], + "measure": { + "kind": "count", + "what": "Drive each save/create/send/delete action and capture the resulting status text (role=status, aria-live regions, toasts, banners). Count messages that contain no noun from the action's object (the button label's object or the page's entity name), or that are only a generic word {success, done, saved, ok, complete}, or that exceed 8 words for a toast. Count must be 0.", + "mustFail": "Saving a customer shows a toast 'Success!'; sending an invoice shows 'Your invoice has been successfully sent to the customer you selected'.", + "mustPass": "'Customer updated'; 'Invoice #1042 sent'." + }, + "exceptions": [ + "When the result is already obvious on screen (the edited item reappears updated, or the user lands on the new item), Primer says no extra success message is needed — screen readers still need it announced", + "Confirmation pages at the end of a transaction carry more: reference number and what happens next" + ], + "kinds": { + "marketing": "maybe", + "content": "no", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Content pages rarely run actions; marketing has at most a sign-up confirmation.", + "agrees": [ + "usability.visibility-of-system-status", + "usability.peak-end-finish-well" + ] + }, + { + "id": "writing.destructive-actions-name-the-consequence", + "claim": "Before a destructive or irreversible action, the confirmation says exactly what will be destroyed and what follows from it, and the title and button repeat the action's verb — 'Delete project', not 'Are you sure?' / 'OK'.", + "source": { + "author": "IBM Carbon Design System", + "work": "Dialog pattern: Confirm a user decision", + "url": "https://carbondesignsystem.com/patterns/dialog-pattern/", + "quote": "Clearly describe the action being confirmed and explain any potential consequences that it may cause. Both the title and the button should reflect the action that will occur." + }, + "evidence": "author", + "prefers": [ + "Title names action and object: 'Delete project Atlas?'", + "Body states the consequence: what is lost, whether it can be undone, what else goes with it", + "Confirm button repeats the verb and object: 'Delete project'", + "For high-impact deletes, have the user type the resource name", + "Do not rely on the red colour alone to signal danger" + ], + "forbids": [ + "'Are you sure?' with no object or consequence", + "'OK' / 'Yes' as the destructive button", + "A danger confirmation for low-impact actions that are trivial to undo" + ], + "measure": { + "kind": "count", + "what": "Trigger every delete/remove/cancel-subscription/reset action and capture the confirmation dialog. Count dialogs where (a) the confirming button's name does not contain the action verb, (b) the title or body does not contain the object's name or type, or (c) the body has no consequence statement (no word from {permanently, cannot be undone, can't be undone, will be deleted, will lose, also delete, removes}). Count must be 0.", + "mustFail": "Clicking 'Delete' on a project opens 'Are you sure?' with buttons 'OK' and 'Cancel'.", + "mustPass": "'Delete project Atlas?' / 'This permanently deletes Atlas and its 14 files. You can't undo this.' / buttons 'Delete project' and 'Cancel'." + }, + "exceptions": [ + "Low-impact deletes that are trivial to undo or recreate should happen without a confirmation, offering Undo instead" + ], + "kinds": { + "marketing": "no", + "content": "no", + "data": "yes", + "workspace": "yes", + "shop": "maybe", + "transaction": "yes" + }, + "kindsWhy": "Marketing and content pages have nothing to destroy; shops only at cart removal or account deletion.", + "agrees": [ + "craft.button-hierarchy-one-primary", + "usability.clearly-marked-emergency-exit" + ] + }, + { + "id": "writing.dates-numbers-units-for-the-reader", + "claim": "Dates spell out the month, numbers are numerals with thousands separators, and units sit a space after their number — formatted in the reader's locale, never as an ambiguous all-numeric date or a raw machine value.", + "source": { + "author": "Shopify Polaris", + "work": "Content: Grammar and mechanics, 'Numbers, dates, and currency'", + "url": "https://polaris.shopify.com/content/grammar-and-mechanics", + "quote": "Use the month’s full name. If there isn’t enough space, use 3-letter abbreviations. Don’t write dates with numerals only." + }, + "evidence": "author", + "prefers": [ + "'December 11, 2024' or 'Dec 11, 2024' (in the reader's locale order)", + "Numerals, not words: 'You have 5 orders to fulfill'", + "Thousands separators: '12,000'", + "A space between number and unit: '3.4 lb', '2 kg'", + "Currency code after the amount when currencies can be confused: '$10,000 USD'", + "Format with Intl.DateTimeFormat / Intl.NumberFormat for the user's locale" + ], + "forbids": [ + "All-numeric dates like '12/11/24'", + "ISO timestamps or epoch values shown raw ('2024-12-11T09:30:00Z')", + "Ordinals in dates ('January 23rd')", + "Unit glued to the number ('3.4lb')", + "Shortened numbers like '12 k' where the exact value matters" + ], + "measure": { + "kind": "count", + "what": "Scan visible text nodes. Count matches of all-numeric dates (\\b\\d{1,2}[/.-]\\d{1,2}[/.-]\\d{2,4}\\b), raw ISO timestamps (\\d{4}-\\d{2}-\\d{2}T\\d{2}:), integers of 5+ digits with no separator outside codes/IDs, and numbers glued to a unit (\\d(kg|lb|cm|mm|km|mi|ml|oz)\\b). Count must be 0.", + "mustFail": "An orders table showing 'Placed 12/11/24', 'Updated 2024-12-11T09:30:00Z', 'Total 12000' and 'Weight 3.4lb'.", + "mustPass": "'Placed Dec 11, 2024', 'Updated Dec 11 at 9:30 am', 'Total 12,000', 'Weight 3.4 lb'." + }, + "exceptions": [ + "Polaris notes these are American English base rules and dates, numbers and measurements should be localized automatically — the target is the reader's locale, not US format", + "Identifiers, codes, SKUs and years are not quantities and take no separator", + "Dense data tables may use compact numeric dates if the format is unambiguous for the locale and stated in the column header" + ], + "kinds": { + "marketing": "maybe", + "content": "yes", + "data": "yes", + "workspace": "yes", + "shop": "yes", + "transaction": "yes" + }, + "kindsWhy": "Marketing pages carry few dates or quantities; every other kind shows them.", + "agrees": [] + } + ] +} diff --git a/skills/uxcli/lenses/workspace.json b/skills/uxcli/lenses/workspace.json index 8b5d354..6c4dc70 100644 --- a/skills/uxcli/lenses/workspace.json +++ b/skills/uxcli/lenses/workspace.json @@ -4,21 +4,33 @@ "name": "Workspace and admin", "when": "Admin, CRUD, tables, settings and B2B tools: repeated work where speed and consistency matter.", "picks": [ + { + "viewpoint": "usability.visibility-of-system-status", + "note": "Same rule, also stated as: modern.feedback-is-local-and-optimistic (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn); writing.success-names-what-happened (Shopify Polaris); data-display.loading-skeleton-holds-the-layout (GitHub Primer); forms.do-not-disable-the-submit-button (GitHub Primer)." + }, + { + "viewpoint": "usability.speak-the-users-language", + "note": "Same rule, also stated as: writing.errors-say-what-and-how-to-fix (GOV.UK Design System (Government Digital Service)); writing.plain-language-reading-level (Shopify Polaris); forms.error-message-says-how-to-fix (GOV.UK Design System); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen)." + }, { "viewpoint": "usability.feedback-within-a-second", - "note": "Same rule, also stated as: modern.interactions-feel-immediate-under-200ms (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn)." + "note": "Same rule, also stated as: modern.interactions-feel-immediate-under-200ms (Rauno Freiberg)." }, { "viewpoint": "usability.signifiers-make-clickable-look-clickable", "note": "Same rule, also stated as: modern.signifiers-survive-flatness (Kate Moran (NN/G)); modern.no-dead-zones-and-honest-clickability (Rauno Freiberg)." }, + { + "viewpoint": "usability.omit-needless-words", + "note": "Same rule, also stated as: writing.sentence-case-ui-text (IBM Carbon Design System)." + }, { "viewpoint": "usability.fitts-target-size-and-distance", "note": "Same rule, also stated as: craft.tap-targets-and-control-height (Erik D. Kennedy); modern.hit-targets-meet-platform-minimums (Apple)." }, { - "viewpoint": "usability.visibility-of-system-status", - "note": "Same rule, also stated as: modern.feedback-is-local-and-optimistic (Rauno Freiberg)." + "viewpoint": "usability.von-restorff-one-emphasis", + "note": "Same rule, also stated as: craft.button-hierarchy-one-primary (Steve Schoger); writing.destructive-actions-name-the-consequence (IBM Carbon Design System)." }, { "viewpoint": "usability.natural-mapping-and-proximity", @@ -29,36 +41,24 @@ "note": "Same rule, also stated as: craft.anything-but-dropdowns (Erik D. Kennedy); modern.boring-and-familiar-beats-novel (Scott Berkun)." }, { - "viewpoint": "usability.inline-validation-after-leaving-field" - }, - { - "viewpoint": "usability.speak-the-users-language" + "viewpoint": "usability.help-in-context", + "note": "Same rule, also stated as: usability.placeholders-are-not-labels (Katie Sherwin, NN/g); forms.hint-text-is-short-and-linked (GOV.UK Design System)." }, { - "viewpoint": "usability.no-false-affordances" + "viewpoint": "usability.inline-validation-after-leaving-field" }, { - "viewpoint": "usability.von-restorff-one-emphasis", - "note": "Same rule, also stated as: craft.button-hierarchy-one-primary (Steve Schoger)." + "viewpoint": "usability.clearly-marked-emergency-exit" }, { - "viewpoint": "usability.omit-needless-words" + "viewpoint": "usability.no-false-affordances" }, { "viewpoint": "usability.aesthetic-usability-effect-bias" }, - { - "viewpoint": "usability.clearly-marked-emergency-exit" - }, - { - "viewpoint": "usability.help-in-context" - }, { "viewpoint": "usability.postel-tolerant-input" }, - { - "viewpoint": "usability.placeholders-are-not-labels" - }, { "viewpoint": "usability.labels-above-fields" }, @@ -73,22 +73,23 @@ }, { "viewpoint": "craft.fewer-borders", - "note": "Same rule, also stated as: canon.gestalt-common-region (Aurora Harley, NN/g); canon.rams-as-little-design-as-possible (Dieter Rams); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); canon.tufte-smallest-effective-difference (Edward Tufte); canon.tufte-one-plus-one-equals-three (Edward Tufte)." + "note": "Same rule, also stated as: canon.gestalt-common-region (Aurora Harley, NN/g); forms.group-related-inputs-in-a-fieldset (U.S. Web Design System (USWDS)); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); modern.cards-are-a-choice-not-a-default (Stan Kirilov); canon.tufte-one-plus-one-equals-three (Edward Tufte); craft.separation-order-space-then-lines-then-boxes (Adam Wathan & Steve Schoger; Erik D. Kennedy)." }, { - "viewpoint": "craft.spacing-and-sizing-system", - "note": "Same rule, also stated as: canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen); modern.spacing-comes-from-a-scale (Stan Kirilov)." + "viewpoint": "craft.dont-overlook-empty-states-teach-by-example", + "note": "Same rule, also stated as: modern.every-state-is-designed (Vercel Labs); writing.empty-states-say-what-next (GitHub Primer); data-display.empty-data-explains-and-offers-next-step (IBM Carbon Design System); data-display.wrap-before-truncating-and-reveal-the-rest (GitHub Primer)." }, { - "viewpoint": "craft.separation-order-space-then-lines-then-boxes" + "viewpoint": "craft.spacing-and-sizing-system", + "note": "Same rule, also stated as: canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.spacing-comes-from-a-scale (Stan Kirilov)." }, { - "viewpoint": "craft.wcag-contrast-and-dont-rely-on-colour-alone", - "note": "Same rule, also stated as: modern.contrast-and-not-colour-alone (Apple)." + "viewpoint": "craft.align-with-readability-in-mind", + "note": "Same rule, also stated as: data-display.right-align-numbers-tabular-figures (GitHub Primer); data-display.text-left-headers-follow-their-column (W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.))." }, { - "viewpoint": "craft.dont-overlook-empty-states-teach-by-example", - "note": "Same rule, also stated as: modern.every-state-is-designed (Vercel Labs)." + "viewpoint": "craft.wcag-contrast-and-dont-rely-on-colour-alone", + "note": "Same rule, also stated as: modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group)." }, { "viewpoint": "craft.hierarchy-is-everything-squint-test" @@ -100,11 +101,14 @@ "viewpoint": "craft.type-scale-few-font-sizes" }, { - "viewpoint": "craft.align-with-readability-in-mind" + "viewpoint": "canon.rams-understandable", + "note": "Same rule, also stated as: modern.no-junk-drawer-menus-or-unlabeled-icons (Jakob Nielsen (NN/G)); writing.buttons-name-the-action (IBM Carbon Design System); usability.dont-make-me-think (Steve Krug); craft.labels-are-a-last-resort (Steve Schoger (attendee notes by ynotdraw))." }, { - "viewpoint": "canon.rams-understandable", - "note": "Same rule, also stated as: modern.no-junk-drawer-menus-or-unlabeled-icons (Jakob Nielsen (NN/G)); usability.dont-make-me-think (Steve Krug); craft.labels-are-a-last-resort (Steve Schoger (attendee notes by ynotdraw))." + "viewpoint": "canon.rams-as-little-design-as-possible" + }, + { + "viewpoint": "canon.tufte-smallest-effective-difference" }, { "viewpoint": "canon.bringhurst-leading-is-a-rhythmic-unit" @@ -116,13 +120,13 @@ "viewpoint": "modern.defaults-are-decisions-you-inherited", "note": "Same rule, also stated as: craft.supercharge-the-defaults (Steve Schoger (attendee notes by ynotdraw))." }, - { - "viewpoint": "modern.cards-are-a-choice-not-a-default" - }, { "viewpoint": "modern.frequent-actions-do-not-animate", "note": "Same rule, also stated as: modern.delight-scales-with-rarity (Benji Taylor)." }, + { + "viewpoint": "modern.numbers-and-text-do-not-shift-layout" + }, { "viewpoint": "modern.motion-values-proportional-to-trigger" }, @@ -144,14 +148,69 @@ { "viewpoint": "modern.focus-is-visible-and-unobscured" }, - { - "viewpoint": "modern.numbers-and-text-do-not-shift-layout" - }, { "viewpoint": "modern.shadows-share-one-light-source" }, { "viewpoint": "modern.glass-and-blur-earn-their-place" + }, + { + "viewpoint": "color.from-tokens-not-hex", + "note": "Same rule, also stated as: color.mode-aware-tokens-in-every-theme (GitHub Primer)." + }, + { + "viewpoint": "color.status-colours-keep-their-meaning" + }, + { + "viewpoint": "color.one-action-colour-apart-from-status" + }, + { + "viewpoint": "color.controls-and-graphics-3-to-1" + }, + { + "viewpoint": "color.focus-ring-contrasts-with-its-surroundings" + }, + { + "viewpoint": "color.few-families-in-proportion" + }, + { + "viewpoint": "writing.one-label-per-action" + }, + { + "viewpoint": "writing.dates-numbers-units-for-the-reader" + }, + { + "viewpoint": "data-display.no-tables-for-layout" + }, + { + "viewpoint": "data-display.one-unit-and-format-per-column" + }, + { + "viewpoint": "data-display.header-cells-are-th-with-scope" + }, + { + "viewpoint": "data-display.long-tables-sort-and-say-so" + }, + { + "viewpoint": "data-display.summary-list-for-key-value-facts" + }, + { + "viewpoint": "forms.error-summary-at-the-top" + }, + { + "viewpoint": "forms.validate-when-the-user-is-done" + }, + { + "viewpoint": "forms.keep-answers-after-an-error" + }, + { + "viewpoint": "forms.mark-optional-fields-in-words" + }, + { + "viewpoint": "forms.input-type-matches-the-answer" + }, + { + "viewpoint": "forms.ask-only-what-you-need" } ] } diff --git a/skills/uxcli/lenses/workspace.md b/skills/uxcli/lenses/workspace.md index d5effbb..daf64f8 100644 --- a/skills/uxcli/lenses/workspace.md +++ b/skills/uxcli/lenses/workspace.md @@ -2,9 +2,33 @@ Admin, CRUD, tables, settings and B2B tools: repeated work where speed and consistency matter. -45 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. +63 viewpoints from named designers. Answer every one for the screen you are looking at: `holds` with where, `breaks` with where and what, `n/a` with why. The sequence, the review file and `uxcli review check` are in `../references/lenses.md`. The rules are the designers', not uxcli's and not yours. -## 1. `usability.feedback-within-a-second` +## 1. `usability.visibility-of-system-status` + +Every action with consequences shows the user something changed, as quickly as possible, so they always know what the system is doing. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #1* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "The design should always keep users informed about what is going on, through appropriate feedback within a reasonable amount of time." +- **Do:** Show a visible trace of every state change; Give feedback immediately, or as quickly as possible; Disable and label a control while its action runs +- **Don't:** Submit silently; Change state with no visible trace; Show an action's result only somewhere else +- **Look at:** Drive an action, then diff DOM or pixels over the next frames: did anything visibly change within 1 s; is there a live region, status text, spinner or busy state? +- **Unless:** Below 0.1 s no special feedback is needed beyond showing the result +- **Also stated as:** modern.feedback-is-local-and-optimistic (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn); writing.success-names-what-happened (Shopify Polaris); data-display.loading-skeleton-holds-the-layout (GitHub Primer); forms.do-not-disable-the-submit-button (GitHub Primer). + +## 2. `usability.speak-the-users-language` + +Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #2* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "The design should speak the users' language. Use words, phrases, and concepts familiar to the user, rather than internal jargon." +- **Do:** Use words the user understands without looking them up; Spell acronyms out in full on each page; Write 'for example', not 'eg' or 'ie' +- **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations +- **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. +- **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se +- **Also stated as:** writing.errors-say-what-and-how-to-fix (GOV.UK Design System (Government Digital Service)); writing.plain-language-reading-level (Shopify Polaris); forms.error-message-says-how-to-fix (GOV.UK Design System); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen). + +## 3. `usability.feedback-within-a-second` Paint something within 0.1 s; keep the user's flow with a response under 1 s; show a progress indicator for anything longer. @@ -14,9 +38,9 @@ Paint something within 0.1 s; keep the user's flow with a response under 1 s; sh - **Don't:** Leave dead time with no indicator; Show a percent-done bar that lies badly - **Look at:** Time from input event to first paint of any change; presence of a progress element, aria-busy, <progress> or role=status update when the wait exceeds 1 s. - **Unless:** Laws of UX claims a purposeful delay can raise perceived value — an opinion with no study cited -- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg); modern.density-is-value-per-time-and-space (Matthew Ström-Awn). +- **Also stated as:** modern.interactions-feel-immediate-under-200ms (Rauno Freiberg). -## 2. `usability.signifiers-make-clickable-look-clickable` +## 4. `usability.signifiers-make-clickable-look-clickable` Links and buttons must look clickable — colour, underline, border or fill — because weak signifiers cost measured time and fixations. @@ -28,7 +52,19 @@ Links and buttons must look clickable — colour, underline, border or fill — - **Unless:** Link position in nav menus or peripheral lists may eliminate the need for underlining (Loranger); Flat works best with low information density, traditional layouts and high-contrast targets positioned standardly (Moran) - **Also stated as:** modern.signifiers-survive-flatness (Kate Moran (NN/G)); modern.no-dead-zones-and-honest-clickability (Rauno Freiberg). -## 3. `usability.fitts-target-size-and-distance` +## 5. `usability.omit-needless-words` + +Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. + +- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) +- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" +- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns +- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case +- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. +- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text +- **Also stated as:** writing.sentence-case-ui-text (IBM Carbon Design System). + +## 6. `usability.fitts-target-size-and-distance` Make targets big, space them apart, and put them where the pointer already is; touch targets at least 1 cm square. @@ -40,19 +76,19 @@ Make targets big, space them apart, and put them where the pointer already is; t - **Unless:** Infinite targets along screen edges for mouse — size matters less at an edge; Primary CTAs, moving users, children and the elderly need larger than the minimum - **Also stated as:** craft.tap-targets-and-control-height (Erik D. Kennedy); modern.hit-targets-meet-platform-minimums (Apple). -## 4. `usability.visibility-of-system-status` +## 7. `usability.von-restorff-one-emphasis` -Every action with consequences shows the user something changed, as quickly as possible, so they always know what the system is doing. +One visually distinct primary action per view, left-aligned with the form, and distinguished by more than colour. -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #1* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "The design should always keep users informed about what is going on, through appropriate feedback within a reasonable amount of time." -- **Do:** Show a visible trace of every state change; Give feedback immediately, or as quickly as possible; Disable and label a control while its action runs -- **Don't:** Submit silently; Change state with no visible trace; Show an action's result only somewhere else -- **Look at:** Drive an action, then diff DOM or pixels over the next frames: did anything visibly change within 1 s; is there a live region, status text, spinner or busy state? -- **Unless:** Below 0.1 s no special feedback is needed beyond showing the result -- **Also stated as:** modern.feedback-is-local-and-optimistic (Rauno Freiberg). +- **Source:** GOV.UK Design System, *Button component* — https://design-system.service.gov.uk/components/button/ +- **In their words:** "Avoid using multiple default buttons on a single page. Having more than one main call to action reduces their impact, and makes it harder for users to know what to do next." +- **Do:** Make key actions visually distinctive; Use restraint so emphases do not compete; Align the primary button to the left edge of the form; Highlight the default, except for dangerous actions +- **Don't:** Put two or more filled primary buttons in one view; Signal emphasis by colour alone +- **Look at:** Cluster interactive elements by computed background, border and weight; the most emphatic cluster per view must have one member; primary's left edge aligns with the inputs; emphasis differs in weight, fill or border, not colour only. +- **Unless:** Do not pre-highlight a dangerous action as the default (Nielsen) +- **Also stated as:** craft.button-hierarchy-one-primary (Steve Schoger); writing.destructive-actions-name-the-consequence (IBM Carbon Design System). -## 5. `usability.natural-mapping-and-proximity` +## 8. `usability.natural-mapping-and-proximity` Put a control next to the thing it changes and a label next to its field; gaps between groups must exceed gaps within them. @@ -64,7 +100,7 @@ Put a control next to the thing it changes and a label next to its field; gaps b - **Unless:** Left-aligned labels are acceptable if space is constrained and labels are of similar length - **Also stated as:** canon.gestalt-proximity (Aurora Harley, NN/g). -## 6. `usability.follow-conventions` +## 9. `usability.follow-conventions` Work the way the sites and platforms users already know; a convention beats a locally optimised novelty. @@ -76,7 +112,19 @@ Work the way the sites and platforms users already know; a convention beats a lo - **Unless:** Desktop apps follow their own OS: Windows OK-first, Apple OK-last - **Also stated as:** craft.anything-but-dropdowns (Erik D. Kennedy); modern.boring-and-familiar-beats-novel (Scott Berkun). -## 7. `usability.inline-validation-after-leaving-field` +## 10. `usability.help-in-context` + +The best help is none; when a field needs explaining, put a short hint beside it at the moment it is needed, not on another page. + +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #10* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "Whenever possible, present the documentation in context right at the moment that the user requires it." +- **Do:** Use hint text for help relevant to most users; Keep hint text to one short sentence without full stops; Associate the hint with its input via aria-describedby +- **Don't:** Put help only on a separate page; Put links inside hint text; Write lengthy explanations +- **Look at:** For inputs with a format expectation (date, postcode, card): a hint associated via aria-describedby and rendered adjacent, same column, within one line-height; hint length in sentences. +- **Unless:** Nielsen's own first sentence: it is best if the system needs no additional explanation +- **Also stated as:** usability.placeholders-are-not-labels (Katie Sherwin, NN/g); forms.hint-text-is-short-and-linked (GOV.UK Design System). + +## 11. `usability.inline-validation-after-leaving-field` Validate a hard field after the user leaves it, never on focus or while typing, and clear the error live once it is fixed. @@ -87,18 +135,18 @@ Validate a hard field after the user leaves it, never on focus or while typing, - **Look at:** Drive a field: focus shows no error; typing an invalid value shows none (unless length threshold reached); blur shows an error adjacent via aria-describedby; typing a fix clears it without blur. - **Unless:** Simple fields need none; premature validation was worse than none (Wroblewski) -## 8. `usability.speak-the-users-language` +## 12. `usability.clearly-marked-emergency-exit` -Write in the words the actor already uses; no internal jargon, error codes or unexplained abbreviations. +Every interaction has a visible, labelled way out — Cancel, Back, Escape, Undo — so a mistaken action does not trap the user. -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #2* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "The design should speak the users' language. Use words, phrases, and concepts familiar to the user, rather than internal jargon." -- **Do:** Use words the user understands without looking them up; Spell acronyms out in full on each page; Write 'for example', not 'eg' or 'ie' -- **Don't:** Show bare error codes; Expose internal entity names; Use unexplained abbreviations -- **Look at:** Flag bare error codes (e.g. ERR-422, Error 0x…), all-caps acronyms not defined on the page, and Latin abbreviations; whether a term is jargon for this actor needs the journey's actor. -- **Unless:** Expert-only tools where the domain term is the users' language — match the user, do not simplify per se +- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #3* — https://www.nngroup.com/articles/ten-usability-heuristics/ +- **In their words:** "Users often perform actions by mistake. They need a clearly marked 'emergency exit' to leave the unwanted action without having to go through an extended process." +- **Do:** Support Undo and Redo; Show a clear Cancel or close control; Label the exit clearly and make it discoverable +- **Don't:** Open a modal with no close; Build a wizard step with no back; Ship a destructive action with no undo +- **Look at:** For each role=dialog or modal: a focusable control whose text or aria-label matches close/cancel/back, and Escape dismisses it; for each step past the first in a flow: a back control exists. +- **Unless:** Legally required interstitials — the exit must still be visible, not necessarily free of consequence -## 9. `usability.no-false-affordances` +## 13. `usability.no-false-affordances` Nothing that is not interactive may look interactive: no underlined or blue static text, no button-shaped badges, no pointer cursor on inert elements. @@ -108,30 +156,7 @@ Nothing that is not interactive may look interactive: no underlined or blue stat - **Don't:** Give static items hyperlink colours; Underline non-interactive text; Make headings or badges resemble buttons - **Look at:** Elements with no href, handler, role or tabindex that have cursor: pointer, underline plus link colour, or a button-like box (border-radius, filled background, short centred text). -## 10. `usability.von-restorff-one-emphasis` - -One visually distinct primary action per view, left-aligned with the form, and distinguished by more than colour. - -- **Source:** GOV.UK Design System, *Button component* — https://design-system.service.gov.uk/components/button/ -- **In their words:** "Avoid using multiple default buttons on a single page. Having more than one main call to action reduces their impact, and makes it harder for users to know what to do next." -- **Do:** Make key actions visually distinctive; Use restraint so emphases do not compete; Align the primary button to the left edge of the form; Highlight the default, except for dangerous actions -- **Don't:** Put two or more filled primary buttons in one view; Signal emphasis by colour alone -- **Look at:** Cluster interactive elements by computed background, border and weight; the most emphatic cluster per view must have one member; primary's left edge aligns with the inputs; emphasis differs in weight, fill or border, not colour only. -- **Unless:** Do not pre-highlight a dangerous action as the default (Nielsen) -- **Also stated as:** craft.button-hierarchy-one-primary (Steve Schoger). - -## 11. `usability.omit-needless-words` - -Cut word count by half: no happy-talk intros, no instruction paragraphs before forms, no marketese, sentence case everywhere. - -- **Source:** Jakob Nielsen, *How Users Read on the Web (1997)* — https://www.nngroup.com/articles/how-users-read-on-the-web/ (study) -- **In their words:** "People rarely read Web pages word by word; instead, they scan the page" -- **Do:** Reduce word count by half compared to traditional writing; Limit paragraphs to one idea each; Use sentence case everywhere except proper nouns -- **Don't:** Open with welcome or happy talk; Put instruction paragraphs before forms; Use promotional language ('marketese'); Set labels in ALL CAPS or Title Case -- **Look at:** Word count between a form's heading and its first input; word count of blocks starting 'Welcome' or 'Thank you for'; buttons and labels in all caps or Title Case; sentence count in instructions. -- **Unless:** Nielsen also asks for outbound links to build trust — brevity is not zero text - -## 12. `usability.aesthetic-usability-effect-bias` +## 14. `usability.aesthetic-usability-effect-bias` Polish makes a screen look more usable than it is; discount your aesthetic impression and run the measurable checks first. @@ -142,28 +167,6 @@ Polish makes a screen look more usable than it is; discount your aesthetic impre - **Look at:** Did the evaluator judge the screen usable because it looks good? Re-check the verdict against the count-kind entries before trusting it; 'attractive' is the variable and stays taste. - **Unless:** With severe usability issues, or functionality sacrificed for aesthetics, users lose patience -## 13. `usability.clearly-marked-emergency-exit` - -Every interaction has a visible, labelled way out — Cancel, Back, Escape, Undo — so a mistaken action does not trap the user. - -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #3* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "Users often perform actions by mistake. They need a clearly marked 'emergency exit' to leave the unwanted action without having to go through an extended process." -- **Do:** Support Undo and Redo; Show a clear Cancel or close control; Label the exit clearly and make it discoverable -- **Don't:** Open a modal with no close; Build a wizard step with no back; Ship a destructive action with no undo -- **Look at:** For each role=dialog or modal: a focusable control whose text or aria-label matches close/cancel/back, and Escape dismisses it; for each step past the first in a flow: a back control exists. -- **Unless:** Legally required interstitials — the exit must still be visible, not necessarily free of consequence - -## 14. `usability.help-in-context` - -The best help is none; when a field needs explaining, put a short hint beside it at the moment it is needed, not on another page. - -- **Source:** Jakob Nielsen, *10 Usability Heuristics for User Interface Design, heuristic #10* — https://www.nngroup.com/articles/ten-usability-heuristics/ -- **In their words:** "Whenever possible, present the documentation in context right at the moment that the user requires it." -- **Do:** Use hint text for help relevant to most users; Keep hint text to one short sentence without full stops; Associate the hint with its input via aria-describedby -- **Don't:** Put help only on a separate page; Put links inside hint text; Write lengthy explanations -- **Look at:** For inputs with a format expectation (date, postcode, card): a hint associated via aria-describedby and rendered adjacent, same column, within one line-height; hint length in sentences. -- **Unless:** Nielsen's own first sentence: it is best if the system needs no additional explanation - ## 15. `usability.postel-tolerant-input` Accept every reasonable form of an input — spaces, dashes, case, whitespace — and normalise it, instead of rejecting it. @@ -175,18 +178,7 @@ Accept every reasonable form of an input — spaces, dashes, case, whitespace - **Look at:** Drive each formatted field with equivalent variants ('4111 1111 1111 1111' vs '4111111111111111', ' a@b.co ', '+44 20…' vs '02…') and compare validation outcomes. - **Unless:** Inputs where ambiguity is dangerous, such as dates in medical or legal contexts -## 16. `usability.placeholders-are-not-labels` - -Every input has a visible label outside the field; placeholder text is never the label, the hint or the only format example. - -- **Source:** Katie Sherwin, NN/g, *Placeholders in Form Fields Are Harmful (2014)* — https://www.nngroup.com/articles/form-design-placeholders/ -- **In their words:** "Disappearing placeholder text strains users' short-term memory." -- **Do:** Give every text input a visible label; Put hints outside empty form fields -- **Don't:** Use placeholder text in place of a label; Use placeholder text for hints or examples; Hide the label visually and rely on the placeholder -- **Look at:** Any input or textarea with a placeholder and no visible associated label, or with a visually hidden label; placeholder used as the only format example. -- **Unless:** A single-field search where the button label carries the meaning is the usual carve-out; the fetched page states none explicitly - -## 17. `usability.labels-above-fields` +## 16. `usability.labels-above-fields` Stack the label above its field, left edges aligned, a few pixels apart, so label and field are read in one fixation. @@ -197,7 +189,7 @@ Stack the label above its field, left edges aligned, a few pixels apart, so labe - **Look at:** For each label/input pair: label box bottom ≤ input box top and label left edge ≈ input left edge within a few px; if side by side, the gap between label right edge and input left edge. - **Unless:** Landscape phones: switch to left-aligned to keep the field visible above the keyboard (Baymard); Left-aligned is acceptable when labels are of similar length and as close to the fields as possible (NN/g) -## 18. `usability.field-width-matches-expected-input` +## 17. `usability.field-width-matches-expected-input` Size each input to the data it expects: short boxes for year, postcode and CVV; one consistent width for variable data like email. @@ -208,7 +200,7 @@ Size each input to the data it expects: short boxes for year, postcode and CVV; - **Look at:** Rendered input width in ch of its font versus maxlength, inputmode, autocomplete token or pattern length: cc-csc wider than ~8 ch, postal-code at full width, or email narrower than ~20 ch. - **Unless:** Full-width inputs on narrow phones are the norm — check at desktop widths or compare relative widths within the form -## 19. `usability.start-with-user-needs-design-with-data` +## 18. `usability.start-with-user-needs-design-with-data` Name the actor and their need before drawing a screen, then let measured behaviour, not hunches or looks, decide what changes. @@ -218,7 +210,7 @@ Name the actor and their need before drawing a screen, then let measured behavio - **Don't:** Design to a hunch; Design to an aesthetic - **Look at:** Does the journey name its actor and need (uxcli context show), and is the design decision traceable to observed behaviour rather than a hunch or a look? -## 20. `usability.constraints-shift-complexity-to-the-system` +## 19. `usability.constraints-shift-complexity-to-the-system` Complexity that cannot be removed goes to the system, not the user: default what can be inferred, hide what is rarely needed. @@ -229,7 +221,7 @@ Complexity that cannot be removed goes to the system, not the user: default what - **Look at:** Count visible required inputs the page could default (no 'same as shipping' control, both address blocks open); presence of autocomplete tokens on address and payment inputs. - **Unless:** Tognazzini's counter-view: users resist complexity reduction and attempt harder tasks when systems get simpler -## 21. `craft.fewer-borders` +## 20. `craft.fewer-borders` Separate elements with space, a background shift, a shadow or striping before reaching for a border; too many borders make a design busy. @@ -239,7 +231,19 @@ Separate elements with space, a background shift, a shadow or striping before re - **Don't:** Use a 1px border as the default grouping device; Wrap a nav in a panel that only needs to recede; Rule every table row with a border when striping would do - **Look at:** For every text node, count ancestors with a visible border, non-none box-shadow or outline; report max nesting depth and bordered boxes per viewport. Counted by `page.nesting`: `review check` refuses a `holds` it contradicts. - **Unless:** Form inputs and the one object a person acts on keep their border; Keylines that make disconnected content feel connected; Decorative borders as style once the count is low -- **Also stated as:** canon.gestalt-common-region (Aurora Harley, NN/g); canon.rams-as-little-design-as-possible (Dieter Rams); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); canon.tufte-smallest-effective-difference (Edward Tufte); canon.tufte-one-plus-one-equals-three (Edward Tufte). +- **Also stated as:** canon.gestalt-common-region (Aurora Harley, NN/g); forms.group-related-inputs-in-a-fieldset (U.S. Web Design System (USWDS)); modern.fewer-borders-more-space (Adam Wathan & Steve Schoger); modern.cards-are-a-choice-not-a-default (Stan Kirilov); canon.tufte-one-plus-one-equals-three (Edward Tufte); craft.separation-order-space-then-lines-then-boxes (Adam Wathan & Steve Schoger; Erik D. Kennedy). + +## 21. `craft.dont-overlook-empty-states-teach-by-example` + +Design the first-load and empty states with sample data or an example, a message and a call to action; never a blank page. + +- **Source:** Erik D. Kennedy, *4 Rules for Intuitive UX* — https://www.learnui.design/blog/4-rules-intuitive-ux.html +- **In their words:** "Use the 'first load' experience to provide sample data, showing by example what the properly-working app will look like" +- **Do:** Show a designed empty state with message, example and call to action; Seed sample data on first load; Show examples rather than descriptions +- **Don't:** Show a totally blank page on first load; Show a table header with no rows and no message +- **Look at:** In the journey's declared empty state, find list or table containers with 0 data children; require a visible sentence inside or adjacent and at least one actionable element. +- **Unless:** Zero-hit search results need a message but no sample data; Transient loading states +- **Also stated as:** modern.every-state-is-designed (Vercel Labs); writing.empty-states-say-what-next (GitHub Primer); data-display.empty-data-explains-and-offers-next-step (IBM Carbon Design System); data-display.wrap-before-truncating-and-reveal-the-rest (GitHub Primer). ## 22. `craft.spacing-and-sizing-system` @@ -251,17 +255,19 @@ Draw every margin, padding and gap from one scale, and make space between groups - **Don't:** Use one-off values such as 13px or 27px; Use equal gaps across a group boundary - **Look at:** Collect all computed margin, padding and gap values above 0; report distinct values and how many are off a 4px grid; compare label-to-field gap with field-to-next-label gap. - **Unless:** Optical adjustments on icons and hanging punctuation sit a pixel or two off-scale -- **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.quality-is-a-choice-spec-is-the-floor (Karri Saarinen); modern.spacing-comes-from-a-scale (Stan Kirilov). +- **Also stated as:** canon.rams-thorough-to-the-last-detail (Dieter Rams); modern.radii-are-few-and-concentric (Vercel Labs); modern.spacing-comes-from-a-scale (Stan Kirilov). -## 23. `craft.separation-order-space-then-lines-then-boxes` +## 23. `craft.align-with-readability-in-mind` -Use the lightest separator that works: more space first, then a keyline or background band, and a box only for the object that is acted on. +Keep few strong left edges, right-align comparable numbers with tabular figures, and hang punctuation so edges line up. -- **Source:** Adam Wathan & Steve Schoger; Erik D. Kennedy, *Refactoring UI (fewer-borders tactic); 7 Rules for Creating Gorgeous UI, Part 1* — https://www.refactoringui.com/ (folklore: the wording is not verified) -- **Do:** Separate with whitespace by default; Add a keyline or background band only when space alone fails; Box only the object a person acts on -- **Don't:** Reach for a card or border first -- **Look at:** Between sibling groups, record which separator is used: gap at least 2x the inner gap (space), hr or border-bottom (line), bordered or shadowed wrapper (box); report the inner/outer gap ratio. -- **Unless:** Dense data such as tables where zebra stripes or keylines are used; Interactive cards that are the unit of action +- **Source:** Steve Schoger, *Little UI Details (tweet, 15 Jun 2017)* — https://digitalsynopsis.com/design/useful-ui-ux-design-tips/ +- **In their words:** "Aligning text is an easy way to clean up your design and make your content much more scannable." +- **Do:** Keep few, strong left edges; Right-align numeric columns with tabular figures; Hang bullets, icons and punctuation so text edges align +- **Don't:** Align a right-aligned image to ragged left-aligned text; Centre columns of numbers +- **Look at:** Count distinct left-edge x positions of text blocks in a container at 1px tolerance; for td cells matching a number or currency pattern, text-align must be right or end and tabular figures must be set. +- **Unless:** IDs, ZIP codes and phone numbers stay left-aligned; Centred layouts under 3 lines +- **Also stated as:** data-display.right-align-numbers-tabular-figures (GitHub Primer); data-display.text-left-headers-follow-their-column (W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.)). ## 24. `craft.wcag-contrast-and-dont-rely-on-colour-alone` @@ -273,21 +279,9 @@ Meet 4.5:1 for body text and 3:1 for headlines, prefer soft backgrounds with dar - **Don't:** Set white text on yellow, green or red fills that fail 4.5:1; Convey status by colour only - **Look at:** Standard contrast ratio of computed text colour against effective background at 4.5:1 or 3:1 by size; status elements must carry a non-colour signal such as text or an icon. - **Unless:** Disabled controls; Logos; Incidental text, per WCAG itself -- **Also stated as:** modern.contrast-and-not-colour-alone (Apple). - -## 25. `craft.dont-overlook-empty-states-teach-by-example` - -Design the first-load and empty states with sample data or an example, a message and a call to action; never a blank page. - -- **Source:** Erik D. Kennedy, *4 Rules for Intuitive UX* — https://www.learnui.design/blog/4-rules-intuitive-ux.html -- **In their words:** "Use the 'first load' experience to provide sample data, showing by example what the properly-working app will look like" -- **Do:** Show a designed empty state with message, example and call to action; Seed sample data on first load; Show examples rather than descriptions -- **Don't:** Show a totally blank page on first load; Show a table header with no rows and no message -- **Look at:** In the journey's declared empty state, find list or table containers with 0 data children; require a visible sentence inside or adjacent and at least one actionable element. -- **Unless:** Zero-hit search results need a message but no sample data; Transient loading states -- **Also stated as:** modern.every-state-is-designed (Vercel Labs). +- **Also stated as:** modern.contrast-and-not-colour-alone (Apple); color.never-the-only-signal (W3C Accessibility Guidelines Working Group). -## 26. `craft.hierarchy-is-everything-squint-test` +## 25. `craft.hierarchy-is-everything-squint-test` Squint: the most important thing must catch the eye first and the least important last; one element dominates each screen. @@ -298,7 +292,7 @@ Squint: the most important thing must catch the eye first and the least importan - **Look at:** Given a declared primary action, score every interactive element as area x contrast x font-weight factor; the declared primary must rank first, and no set of buttons may sit within 10% of each other. - **Unless:** Page titles are the only element styled all-out up-pop; A browsing page such as a gallery may have no single most important thing -## 27. `craft.greys-dont-have-to-be-grey-never-use-black` +## 26. `craft.greys-dont-have-to-be-grey-never-use-black` Tint the neutral scale toward the brand hue and never use pure black for text. @@ -309,7 +303,7 @@ Tint the neutral scale toward the brand hue and never use pure black for text. - **Look at:** Count text, border and background colours with saturation 0 and lightness under 15%, and the share of neutral swatches with saturation exactly 0. - **Unless:** It does not always work; worth a trial; High-contrast or accessibility modes and print -## 28. `craft.type-scale-few-font-sizes` +## 27. `craft.type-scale-few-font-sizes` Use about four font sizes from a fixed scale; reuse the default size for body, menus, lists and controls. @@ -320,18 +314,7 @@ Use about four font sizes from a fixed scale; reuse the default size for body, m - **Look at:** Count distinct computed font-size values on visible text per viewport and how many fall outside the declared scale. - **Unless:** Sizes must stay distinguishable, so large text may step many points apart; Marketing pages may add a display size -## 29. `craft.align-with-readability-in-mind` - -Keep few strong left edges, right-align comparable numbers with tabular figures, and hang punctuation so edges line up. - -- **Source:** Steve Schoger, *Little UI Details (tweet, 15 Jun 2017)* — https://digitalsynopsis.com/design/useful-ui-ux-design-tips/ -- **In their words:** "Aligning text is an easy way to clean up your design and make your content much more scannable." -- **Do:** Keep few, strong left edges; Right-align numeric columns with tabular figures; Hang bullets, icons and punctuation so text edges align -- **Don't:** Align a right-aligned image to ragged left-aligned text; Centre columns of numbers -- **Look at:** Count distinct left-edge x positions of text blocks in a container at 1px tolerance; for td cells matching a number or currency pattern, text-align must be right or end and tabular figures must be set. -- **Unless:** IDs, ZIP codes and phone numbers stay left-aligned; Centred layouts under 3 lines - -## 30. `canon.rams-understandable` +## 28. `canon.rams-understandable` The structure of the product should be visible and its controls legible without instruction. @@ -341,7 +324,29 @@ The structure of the product should be visible and its controls legible without - **Don't:** Rely on tooltips as the only explanation; Ship icon-only controls without a visible label; Require a manual - **Look at:** Interactive elements with no accessible name; icon-only controls without a visible text label; landmark regions with no heading. - **Unless:** Rams's own products rely on convention rather than labels everywhere; the rule is about structure, not text density -- **Also stated as:** modern.no-junk-drawer-menus-or-unlabeled-icons (Jakob Nielsen (NN/G)); usability.dont-make-me-think (Steve Krug); craft.labels-are-a-last-resort (Steve Schoger (attendee notes by ynotdraw)). +- **Also stated as:** modern.no-junk-drawer-menus-or-unlabeled-icons (Jakob Nielsen (NN/G)); writing.buttons-name-the-action (IBM Carbon Design System); usability.dont-make-me-think (Steve Krug); craft.labels-are-a-last-resort (Steve Schoger (attendee notes by ynotdraw)). + +## 29. `canon.rams-as-little-design-as-possible` + +Remove everything that does not serve the purpose; less, but better. + +- **Source:** Dieter Rams, *Ten principles for good design* — https://www.vitsoe.com/us/about/good-design +- **In their words:** "Good design is as little design as possible. Less, but better – because it concentrates on the essential aspects, and the products are not burdened with non-essentials." +- **Do:** Remove anything that does not serve the purpose; Keep one border where one will do +- **Don't:** Add ornament or decoration for its own sake; Repeat the heading as a decorative icon; Wrap content in extra bordered or shadowed layers +- **Look at:** Decorative-only elements (no text, image or interactive role, only border/shadow/background) and distinct box-shadow, border-radius and gradient declarations in computed styles. +- **Unless:** Aesthetic quality is part of usefulness (principle 3): stripping to nothing is not the goal + +## 30. `canon.tufte-smallest-effective-difference` + +Make every visual distinction as subtle as it can be while still reading clearly. + +- **Source:** Edward Tufte, *Visual Explanations (1997), p.73* — https://boxesandarrows.com/three-lessons-from-tufte-special-deliverable-6/ (secondary) +- **In their words:** "Make all visual distinctions as subtle as possible, but still clear and effective." +- **Do:** Light grey before dark grey, grey before colour; Dividers and borders lighter than body text; Mute secondary elements +- **Don't:** Black borders and saturated dividers around quiet content; Secondary elements with more contrast than the primary content +- **Look at:** Luminance contrast of non-data, non-text elements (borders, dividers, gridlines, rules) against their background; structural lines above a lens threshold such as 3:1; secondary elements exceeding the primary content's contrast. +- **Unless:** The distinction must still be clear and effective; WCAG text contrast is a floor this rule must not undercut ## 31. `canon.bringhurst-leading-is-a-rhythmic-unit` @@ -376,18 +381,7 @@ A library or AI default is someone else's decision; commit to one written aesthe - **Unless:** A purple brand is allowed to be purple; the tell is the constellation and the absence of a decision, not any one colour - **Also stated as:** craft.supercharge-the-defaults (Steve Schoger (attendee notes by ynotdraw)). -## 34. `modern.cards-are-a-choice-not-a-default` - -Cards are for browsing; use lists for searching and tables for comparing, and keep each card readable with one primary action. - -- **Source:** Stan Kirilov, *UI Card Design (StanVision journal)* — https://www.stan.vision/journal/ui-card-design-examples-best-practices-and-common-patterns (secondary) -- **In their words:** "Cards for browsing. Lists for searching. Tables for comparing." -- **Do:** Lists for ranked or homogeneous data, tables for comparison; Consistent card heights; The title as the link, with a pseudo-element extending the hit area -- **Don't:** Card grids for search results and settings; Whole-card anchor wrappers; More than 3 clickable elements per card; Nested cards -- **Look at:** Detect card grids (3+ siblings with equal border, shadow and radius); fail on text under 14px, over 3 interactive descendants per card, row heights differing over 20%, or an anchor wrapping more than heading plus paragraph. -- **Unless:** Masonry where variable height is the design choice; Media browsing such as Netflix or Pinterest - -## 35. `modern.frequent-actions-do-not-animate` +## 34. `modern.frequent-actions-do-not-animate` Actions used many times a day, and anything keyboard-triggered, appear instantly without an enter animation. @@ -399,6 +393,17 @@ Actions used many times a day, and anything keyboard-triggered, appear instantly - **Unless:** macOS context menus fade out and blink the chosen item; Rare features may be theatrical (delight-impact curve) - **Also stated as:** modern.delight-scales-with-rarity (Benji Taylor). +## 35. `modern.numbers-and-text-do-not-shift-layout` + +Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. + +- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ +- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" +- **Do:** tabular-nums in tables, timers and prices; width and height on every img +- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover +- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. +- **Unless:** Prose numbers + ## 36. `modern.motion-values-proportional-to-trigger` Scale and fade motion starts near its resting size, in proportion to the trigger, never from zero or a heavy squash. @@ -475,18 +480,7 @@ Every focusable element shows a visible focus ring on :focus-visible, and no sti - **Don't:** outline: none or 0 with no replacement; Focus rings hidden under sticky headers - **Look at:** Tab through every focusable element; diff the focused and unfocused rect or read computed outline and box-shadow under :focus-visible; check the focused rect is not intersected by position fixed or sticky elements above it. -## 43. `modern.numbers-and-text-do-not-shift-layout` - -Numbers in columns and timers use tabular figures, images carry dimensions, and nothing changes weight on hover. - -- **Source:** Rauno Freiberg, *Web Interface Guidelines* — https://interfaces.rauno.me/ -- **In their words:** "tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers" -- **Do:** tabular-nums in tables, timers and prices; width and height on every img -- **Don't:** Proportional digits in columns; Images without dimensions; Weight changes on hover -- **Look at:** Numeric td cells whose computed font-variant-numeric lacks tabular-nums; img without width/height or aspect-ratio; sibling rect drift while a counter fixture runs. -- **Unless:** Prose numbers - -## 44. `modern.shadows-share-one-light-source` +## 43. `modern.shadows-share-one-light-source` All shadows on a page share one light direction and offset ratio, layered and tinted toward the background hue. @@ -497,7 +491,7 @@ All shadows on a page share one light direction and offset ratio, layered and ti - **Look at:** Collect every computed box-shadow; fail when offset signs mix across the page or distinct shadow strings exceed about 6; warn when shadow hue is far from the background hue with alpha above 0.5. - **Unless:** Inset shadows for sunken fields; Glows meant as glows -## 45. `modern.glass-and-blur-earn-their-place` +## 44. `modern.glass-and-blur-earn-their-place` Translucency is for chrome over busy backgrounds with heavy blur and a contrast fallback, never for body content or decoration. @@ -507,3 +501,213 @@ Translucency is for chrome over busy backgrounds with heavy blur and a contrast - **Don't:** Glass cards for body content; Low blur over photos; Text whose contrast depends on what scrolls beneath it; Glass with a neon glow as decoration - **Look at:** For each element with backdrop-filter, sample contained-text contrast against the composited background at three scroll positions; any under 4.5:1 fails; blur under 12px over an image warns; body text over 2 lines inside glass warns. - **Unless:** OS-level materials that honour the user's Reduce Transparency setting + +## 45. `color.from-tokens-not-hex` + +Every colour on the page comes from the design system's named tokens or palette functions, never from hex values copied into components. + +- **Source:** GOV.UK Design System (Government Digital Service), *Styles: Colour* — https://design-system.service.gov.uk/styles/colour/ +- **In their words:** "Do not copy the specific hexadecimal (hex) colour values." +- **Do:** Reference colour by role token (brand, text, error, border) rather than by value; Use palette colours (tints and shades of a few families) for supporting elements; Use a functional token only in the context it is designed for +- **Don't:** Hex literals in component styles; Near-duplicate colours (#1d70b8 next to #1d70b9) created by eye-dropping; Using the error token as a general red +- **Look at:** Collect every computed color, background-color, border-*-color, outline-color and fill/stroke of painted elements, and every value of CSS custom properties declared on :root (and on any theme selector). Count distinct painted colours that match no custom-property value (exact RGBA after resolution), and count pairs of painted colours closer than ΔE 2 that are not identical. +- **Unless:** Images, illustrations and embedded third-party widgets; Browser defaults on unstyled native controls; GOV.UK: palette colours (not functional ones) are allowed for illustrations and custom components +- **Also stated as:** color.mode-aware-tokens-in-every-theme (GitHub Primer). + +## 46. `color.status-colours-keep-their-meaning` + +Each status colour has one meaning across the product (critical for errors and blocked actions, warning for what needs attention, success for what went well, info for tips) and is never borrowed for promotion or decoration. + +- **Source:** Shopify Polaris, *Colors: Palettes and roles (Critical, Success)* — https://polaris.shopify.com/design/colors/palettes-and-roles +- **In their words:** "Elements using critical must convey messaging that implies that an action is impossible, blocked, or has resulted in an error." +- **Do:** Map error, warning, success and info to named roles or tokens and use them only in those roles; Reserve the critical red for errors, blocked actions and destructive buttons; Use the info role, not warning or critical, for tips and announcements +- **Don't:** A sale or 'new' badge in the error red; Success green used to entice or to advertise an offer; Warning colour for 'coming soon' or 'under construction' messaging; Two different reds meaning error on different screens +- **Look at:** Find the colour the page uses for error text (an element with role=alert, aria-invalid's described-by message, or a class/token named error/critical/danger) and the success colour likewise. Count painted elements (text, fill or border) whose colour equals that error or success colour, within a ΔE of 3, and that are neither a validation message, an invalid field, a status badge of that meaning, nor a destructive action. +- **Unless:** Brand colours that happen to be red, as long as a separate, distinct error red is used for errors; Data visualisations where a series colour coincides with a status hue but no status is implied (prefer avoiding it) + +## 47. `color.one-action-colour-apart-from-status` + +Links and primary actions share one interactive colour family, used consistently, and that colour is not the colour of errors, warnings or success. + +- **Source:** IBM Carbon Design System, *Elements: Color, Overview (Color anatomy)* — https://carbondesignsystem.com/elements/color/overview/ +- **In their words:** "The core blue family serves as the primary action color across all IBM products and experiences. Additional colors are used sparingly and purposefully." +- **Do:** One link colour, used only for links; Primary buttons in the brand or action colour; Danger colour reserved for destructive buttons, not for the primary action +- **Don't:** Links in the error red; Primary buttons in several different hues across screens; Static text coloured like links +- **Look at:** Take the computed text colour of every a[href] in running text and the background of every primary (first, filled, or type=submit) button. Count distinct hues among them (30° bins), and count links or primary buttons whose colour is within ΔE 10 of the page's error, warning or success colour. Also count non-interactive text elements painted in the link colour. +- **Unless:** Primer (GitHub) deliberately uses the success role for primary buttons; a system that states such a mapping consistently is following its own rule; Destructive primary actions (Delete account) take the danger colour on purpose; Navigation menus, where position signals the link + +## 48. `color.controls-and-graphics-3-to-1` + +The parts that show a control is there and what state it is in (input borders, checkbox boxes and ticks, toggle tracks, icon-only buttons, meaningful chart marks) contrast at least 3:1 with the colours next to them. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "Unless the control is inactive, any visual information provided that is necessary for a user to identify that a control is present and how to operate it must have a minimum 3:1 contrast ratio with the adjacent colors." +- **Do:** Input borders at 3:1 against the background the input sits on, or a filled input background at 3:1; Checkbox ticks and radio dots at 3:1 against the box; Standalone icons at 3:1 against their background; Avoid very thin lines that anti-alias below the nominal ratio +- **Don't:** Pale grey input borders (#ddd on white is about 1.4:1); A selected state shown only by a faint tint; Hover effects that lower a control's contrast with its surroundings +- **Look at:** For each visible input, select, textarea, checkbox, radio, [role=switch] and icon-only button (no visible text): compute the contrast ratio between the colour that identifies it (border colour, or its own background when it has no border, or the icon fill) and the background behind it; count those below 3:1, unrounded. Disabled controls are skipped. +- **Unless:** Inactive (disabled) controls are exempt; A control identified by its visible text needs no contrasting boundary; Logos and decorative graphics; Appearance determined by the browser and not modified by the author + +## 49. `color.focus-ring-contrasts-with-its-surroundings` + +The focus indicator contrasts at least 3:1 with whatever it is drawn against: the page background for an outer ring, the component's own colours for an inner one. + +- **Source:** W3C Accessibility Guidelines Working Group, *Understanding Success Criterion 1.4.11: Non-text Contrast, Relationship with Focus Visible (WCAG 2.2)* — https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html +- **In their words:** "In combination with 2.4.7 Focus Visible, the visual focus indicator for a component must have sufficient contrast against the adjacent background when the component is focused, except where the appearance of the component is determined by the user agent and not modified by the author." +- **Do:** An outer ring that contrasts with the page background; A two-colour ring (dark and light) that holds on any background; A thick indicator rather than a 1px one +- **Don't:** A yellow outer ring on a white page; A focus border that changes hue inside the component without contrasting with its fill; Focus shown only by a background tint change +- **Look at:** Tab to each focusable control; diff the focused and unfocused screenshots of the control's box padded by a few pixels; for the changed pixels, take their colour and the colour of the unchanged pixels adjacent to them (page background outside, component fill inside). Count controls where no changed region reaches 3:1 against its adjacent colour. page.focus-visible checks only that some pixel changes; this checks that the change can be seen. +- **Unless:** Unmodified browser default focus styles; WCAG does not compare focused and unfocused states with each other; a background-only change is out of scope for 1.4.11 but fails Use of Color + +## 50. `color.few-families-in-proportion` + +Use a few colour families in a deliberate proportion (neutral base dominant, then primary, secondary and a small accent), not an even spread of many hues. + +- **Source:** U.S. Web Design System (GSA), *Design tokens: Theme color tokens* — https://designsystem.digital.gov/design-tokens/color/theme-tokens/ +- **In their words:** "about 60% of your site’s color would be the primary color family, about 30% would be the secondary color family, and about 10% would be the accent color families" +- **Do:** Neutral base for text and most surfaces; One primary family carrying most of the colour, one secondary, a small accent; Additional colours used sparingly and for a purpose (Carbon); Start in black and white, then add colour to support the message (USWDS) +- **Don't:** Five or more saturated hue families at similar weight on one screen; An accent that covers more area than the primary; A new hue introduced for a single component +- **Look at:** Screenshot the page and bucket every non-neutral pixel (HSL saturation above about 15%) by hue into 30° bins, ignoring images and status colours. How many hue families take more than 2% of the coloured area, and does the largest one carry most of it while the smallest (accent) stays near a tenth? +- **Unless:** USWDS: the proportions are for non-base colours; neutral text will usually dominate; Illustration, photography and data visualisation, which need their own palettes; Brands whose identity is multi-hue + +## 51. `writing.one-label-per-action` + +One concept, one word: controls that do the same thing carry the same label everywhere, and controls that do different things never share a label. + +- **Source:** GitHub Primer, *Accessibility guide: Descriptive buttons, 'How to test names'* — https://primer.style/guides/accessibility/descriptive-buttons +- **In their words:** "When buttons perform the same action, they have the same name." +- **Do:** Keep a terminology list of preferred words and words not to use for the product; Pick one verb per action (Delete or Remove, not both for the same thing) and reuse it across pages; Add the object to disambiguate repeated actions ('Remove Apples', 'Remove Pears') +- **Don't:** Synonyms for one action across screens: 'Save' here, 'Update' there, 'Apply' elsewhere; Identical labels for different actions on the same page; Naming the same object two ways ('workspace' and 'project') in one product +- **Look at:** Across the journey's pages, collect (accessible name, action) pairs for buttons and links, where action is the form action/href/handler target. Count names that map to two or more different actions on one page, and actions reached by two or more different names across pages; also flag known synonym pairs present together (save/update/apply, delete/remove, sign in/log in, cart/basket). Count must be 0. +- **Unless:** Delete and Remove may coexist when they mean different things (destroy vs take out of a collection), as Carbon defines them — then each must be used only for its own meaning + +## 52. `writing.dates-numbers-units-for-the-reader` + +Dates spell out the month, numbers are numerals with thousands separators, and units sit a space after their number — formatted in the reader's locale, never as an ambiguous all-numeric date or a raw machine value. + +- **Source:** Shopify Polaris, *Content: Grammar and mechanics, 'Numbers, dates, and currency'* — https://polaris.shopify.com/content/grammar-and-mechanics +- **In their words:** "Use the month’s full name. If there isn’t enough space, use 3-letter abbreviations. Don’t write dates with numerals only." +- **Do:** 'December 11, 2024' or 'Dec 11, 2024' (in the reader's locale order); Numerals, not words: 'You have 5 orders to fulfill'; Thousands separators: '12,000'; A space between number and unit: '3.4 lb', '2 kg'; Currency code after the amount when currencies can be confused: '$10,000 USD'; Format with Intl.DateTimeFormat / Intl.NumberFormat for the user's locale +- **Don't:** All-numeric dates like '12/11/24'; ISO timestamps or epoch values shown raw ('2024-12-11T09:30:00Z'); Ordinals in dates ('January 23rd'); Unit glued to the number ('3.4lb'); Shortened numbers like '12 k' where the exact value matters +- **Look at:** Scan visible text nodes. Count matches of all-numeric dates (\b\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}\b), raw ISO timestamps (\d{4}-\d{2}-\d{2}T\d{2}:), integers of 5+ digits with no separator outside codes/IDs, and numbers glued to a unit (\d(kg|lb|cm|mm|km|mi|ml|oz)\b). Count must be 0. +- **Unless:** Polaris notes these are American English base rules and dates, numbers and measurements should be localized automatically — the target is the reader's locale, not US format; Identifiers, codes, SKUs and years are not quantities and take no separator; Dense data tables may use compact numeric dates if the format is unambiguous for the locale and stated in the column header + +## 53. `data-display.no-tables-for-layout` + +A table is for comparing data in rows and columns, never for arranging content on the page; layout belongs to the grid. + +- **Source:** GOV.UK Design System (Government Digital Service), *Table — When not to use this component* — https://design-system.service.gov.uk/components/table/ +- **In their words:** "Never use the table component to layout content on a page." +- **Do:** CSS grid or flex for page and dashboard layout; A list, cards or a summary list for items that do not share columns; Tables only where every row has the same fields +- **Don't:** A table that positions a sidebar, form or dashboard tiles; Table cells holding headings, paragraphs or whole forms; role=presentation on a table that actually holds tabular data +- **Look at:** Count table elements that have no th and either contain headings, more than one paragraph per cell, form fieldsets or nested tables, or have a single row whose cells hold unrelated content blocks. +- **Unless:** HTML email, where tables are still the only reliable layout tool + +## 54. `data-display.one-unit-and-format-per-column` + +Every value in a column uses the same unit, precision and format; the unit is named once in the header, not repeated in each cell. + +- **Source:** U.S. Web Design System (GSA), *Table — Usability guidance* — https://designsystem.digital.gov/components/table/ +- **In their words:** "Predictably format columns. Take care not to vary units or formatting within the same column. Instead, normalize values so they can be easily compared." +- **Do:** Normalise a column to one unit (all days, not days and weeks); Keep the same number of decimal places in every row of a column; Put the unit or symbol in the column header ('Temperature °C') rather than in every cell; Use one date format and one thousands separator throughout the table +- **Don't:** Mixing '3 days' and '2 weeks' in one column; '12.5', '12.50' and '12.500' in the same column; Abbreviating some values ('1.2k') and not others in the same column +- **Look at:** For each column of numeric td, count distinct decimal-place counts, distinct unit suffixes or prefixes, and distinct date formats; any column with more than one is a failure. +- **Unless:** A column deliberately mixing magnitudes where the unit changes are labelled per cell and sorting is by underlying value — rare, and worth questioning + +## 55. `data-display.header-cells-are-th-with-scope` + +A data table marks its header cells as th and its data cells as td; when headers run both across and down, scope (or id/headers in complex tables) ties each cell to its headers. + +- **Source:** W3C WAI (Eric Eggert, Shadi Abou-Zahra, eds.), *Tables Tutorial* — https://www.w3.org/WAI/tutorials/tables/ +- **In their words:** "Header cells must be marked up with <th>, and data cells with <td> to make tables accessible. For more complex tables, explicit associations may be needed using scope, id, and headers attributes." +- **Do:** A thead row of th scope=col in every data table; th scope=row for the first cell when it names the row's item; id and headers attributes when a cell sits under more than one level of header; A caption that names the table +- **Don't:** Header rows built from td styled bold; Div grids that look like tables with no role=table/row/columnheader/cell semantics; One table holding several topics separated by extra rows of th +- **Look at:** Count table (and role=table/grid) elements that have more than one row of data and no th / role=columnheader; count tables with both a header row and a header column whose th lack scope; count tables with no caption, aria-label or aria-labelledby. +- **Unless:** A simple table with one header row needs only th, not scope; scope matters once there are header rows and header columns; Layout tables, which should not exist (see data-display.no-tables-for-layout) + +## 56. `data-display.long-tables-sort-and-say-so` + +Long tables let users sort the columns that have a natural order, start with one column sorted, and expose the sort state to assistive technology. + +- **Source:** U.S. Web Design System (GSA), *Table — Usability guidance* — https://designsystem.digital.gov/components/table/ +- **In their words:** "Enable sort where useful. Add row sorting to individual columns of long tables where the data can be logically ordered either alphabetically or numerically." +- **Do:** Sort controls as buttons inside the th of sortable columns; One column sorted on load, shown with an arrow and aria-sort on its th (Primer); A polite live region that announces the new sort (USWDS); A filter or search field above long tables, and pagination for very large sets +- **Don't:** Long tables with no sort, filter or pagination; A sort state shown only by an icon with no aria-sort; Row sorting on tables with merged (colspan/rowspan) cells +- **Look at:** Count tables with more than 20 body rows (or with pagination) that have no sort control in any th and no filter/search input associated with them; among sortable tables, count those where no th carries aria-sort other than none. +- **Unless:** Short tables a user takes in at a glance; Tables whose row order is itself the meaning (a ranked list, a timetable) may fix the order + +## 57. `data-display.summary-list-for-key-value-facts` + +A set of facts about one thing — label and value pairs — is shown as a summary list (dl with dt and dd), not as a table with no headers or as loose text. + +- **Source:** GOV.UK Design System (Government Digital Service), *Summary list — When to use / When not to use this component* — https://design-system.service.gov.uk/components/summary-list/ +- **In their words:** "Use a summary list to show information as a list of key facts. … only use it to present information that has a key and at least one value." +- **Do:** dl with dt (the key) and dd (the value) for record details, metadata and check-your-answers pages; A row action ('Change') whose accessible name includes the key ('Change name'); Headings or cards to separate several summary lists on one page +- **Don't:** A two-column table without th used to show one record's fields; Key–value pairs set as 'Label: value' runs in a paragraph; A summary list for genuinely tabular data or a plain list of items +- **Look at:** Where the screen shows the fields of a single record (a profile, an order, an item's metadata, a check-answers page), are the label–value pairs marked up as dl/dt/dd with each key visually distinct from its value — and is tabular data comparing several records in a table rather than a summary list? +- **Unless:** Two or three facts inside a card may be inline text if they are not scanned as a set + +## 58. `forms.error-summary-at-the-top` + +After a failed submit, show an error summary at the top of the page that takes focus and links each error to its field — even when there is only one error. + +- **Source:** GOV.UK Design System, *Error summary component* — https://design-system.service.gov.uk/components/error-summary/ +- **In their words:** "Always show an error summary when there is a validation error, even if there’s only one." +- **Do:** A summary above the h1 (below any back link) with a heading such as 'There is a problem'; Move keyboard focus to the summary when it appears; One link per error, pointing at the field (or the first field of a date or radio group); Word each summary item exactly like the message beside its field; Prefix the page <title> with 'Error: ' +- **Don't:** Errors shown only beside fields far down a long form, with focus left on the submit button; A summary of plain text with no links to the fields; A toast or banner that disappears before it can be read +- **Look at:** Submit the form empty. Within 1 s: is there an element above the first h1 of main that has focus (document.activeElement inside it), contains one link per invalid field, and does each link's href resolve to the id of an invalid input? Does document.title start with 'Error'? +- **Unless:** Primer suggests the interactive summary only for 3 or more errors, and otherwise focusing the first invalid field; A one-field form (search, single email sign-up) can rely on the message beside the field + +## 59. `forms.validate-when-the-user-is-done` + +Do not show an error while the user is still typing; validate when they try to continue, and only add earlier validation where research shows it helps. + +- **Source:** GOV.UK Design System, *Recover from validation errors pattern — When to tell the user about validation errors* — https://design-system.service.gov.uk/patterns/validation/ +- **In their words:** "Generally speaking, avoid validating the information in a field before the user has finished entering it. This sort of validation can cause problems - especially for users who type more slowly." +- **Do:** Validate on Continue or Submit; After a failed submit, update a field's error live once the user fixes it (Primer); A character count is the accepted exception: warn as the limit is passed +- **Don't:** An error that appears on the first keystroke of an email or phone field; Red borders on an untouched form at load; Browser-native HTML5 validation bubbles in place of designed messages +- **Look at:** Focus an email or formatted field and type one character, keeping focus. Wait 1 s. Does any error message, aria-invalid=true or error colour appear before blur or submit? Also load the form fresh: are any fields already marked invalid? +- **Unless:** GOV.UK goes further and says not to validate on blur either; Baymard (usability.inline-validation-after-leaving-field) recommends blur validation for hard fields — the schools agree only on 'not while typing'; Primer allows validating as the user types once the field has already been flagged invalid, so the error clears as soon as it is fixed + +## 60. `forms.keep-answers-after-an-error` + +When a submit fails, show the form again with every answer the user gave still in it — the failing ones and the passing ones. + +- **Source:** GOV.UK Design System, *Error message component — How it works* — https://design-system.service.gov.uk/components/error-message/ +- **In their words:** "Do not clear any form fields when showing the Error message component. Keep both passing and failing answers." +- **Do:** Re-render server-side errors with submitted values filled in; Keep the failing value so the user can see and edit what went wrong; Pre-populate fields when the user goes back to change an answer +- **Don't:** A reload that empties the form after a server error; Clearing the field that failed; Clearing password-adjacent fields such as name and email along with the password +- **Look at:** Fill every field with valid values except one, submit, and after the error page paints read each input's value: how many fields that had a value now read empty? +- **Unless:** Password and card security code fields may be cleared for security, and the message should say so + +## 61. `forms.mark-optional-fields-in-words` + +Ask mostly required questions and label the exceptions '(optional)' in words; do not mark required fields with asterisks. + +- **Source:** GOV.UK Design System, *Question pages pattern* — https://design-system.service.gov.uk/patterns/question-pages/ +- **In their words:** "in most contexts, add ‘(optional)’ to the labels of optional fields" +- **Do:** '(optional)' in the label, or in the legend for a radio or checkbox group; Removing optional questions before marking them; One convention across the whole service +- **Don't:** Asterisks on mandatory fields; Optional fields that look identical to required ones; Required and optional marked differently on different pages +- **Look at:** Submit the form with every field empty and note which fields raise no error (optional). Count optional fields whose label or legend text lacks 'optional', and labels that contain '*'. +- **Unless:** USWDS instead marks required fields with a red asterisk explained by a note at the top, and also labels optional fields '(optional)' — the schools agree that optional fields say so in words; Primer and USWDS exempt one-field forms and login forms, where every field is plainly required + +## 62. `forms.input-type-matches-the-answer` + +Give each field the type and on-screen keyboard its answer needs — email, tel, inputmode numeric or decimal for numbers — and avoid type=number. + +- **Source:** GOV.UK Design System, *Text input component — Numbers* — https://design-system.service.gov.uk/components/text-input/ +- **In their words:** "With <input type="number"> there’s a risk of users accidentally incrementing a number when they’re trying to do something else - for example, scroll up or down the page." +- **Do:** type='email' with spellcheck=false for email addresses; type='tel' for phone numbers; type='text' with inputmode='numeric' for whole numbers and codes, inputmode='decimal' for amounts; spellcheck=false on names, references and codes +- **Don't:** type='number' for card numbers, postcodes, account numbers or reference codes; type='text' with no inputmode on a field that only accepts digits; Blocking paste into any field +- **Look at:** For each input, read type and inputmode against its label: count email fields not type=email, phone fields not type=tel, digit-only fields (amount, quantity, code, card, account) with neither inputmode numeric/decimal nor type=tel, and any type=number. +- **Unless:** type=number when research shows a need, such as a stepper on a small bounded quantity; Negative amounts: GOV.UK advises type=text without inputmode, since some keypads have no minus key + +## 63. `forms.ask-only-what-you-need` + +Every question on the form has a named use; drop the ones nobody can justify, and never ask for the same thing twice in one journey. + +- **Source:** GOV.UK Design System, *Question pages pattern* — https://design-system.service.gov.uk/patterns/question-pages/ +- **In their words:** "You should make sure you know why you’re asking every question and only ask users for information you really need." +- **Do:** A question protocol: for each field, who uses the answer and what for; Reuse an earlier answer by pre-filling it rather than asking again; Let 'I do not know' be an answer where it is a valid one; Say why a sensitive question is asked, in its hint +- **Don't:** 'Nice to have' fields such as title, gender or 'how did you hear about us' with no stated use; Asking for an email or address a second time in the same journey; Confirm-email fields that make the user type the address twice, instead of playing it back for checking +- **Look at:** List every field on the form. For each, can the team name who reads the answer and what decision it changes? How many fields have no answer, and how many repeat information given earlier in the journey? +- **Unless:** Legal or regulatory questions whose use is mandated, which should still say why in a hint diff --git a/skills/uxcli/references/research.md b/skills/uxcli/references/research.md new file mode 100644 index 0000000..e98ccd5 --- /dev/null +++ b/skills/uxcli/references/research.md @@ -0,0 +1,61 @@ +# research — learning the domain before you design for it + +Read when: you are about to design for a product whose field you do not already know from sources — +its vocabulary, its rules, what its people do all day and what goes wrong for them. A screen designed +from what a model assumes about a field looks plausible and fails the person who works in it. This is +how you find out first, and how you write down what you found so the next agent does not have to. + +## Start from the kind of product + +```bash +npx -y @junixlabs/uxcli template # workspace, shop, landing +npx -y @junixlabs/uxcli template show <id> # screens and their lens, journeys to walk, what to research +npx -y @junixlabs/uxcli template apply <id> # the actor questions, written as unknowns (creates only) +``` + +A template is where research starts, not what it found: every line about people in it is a question. +If no template fits, take the closest one's questions as a checklist and say which you skipped. + +## Where to look, in order of how much it can carry + +| Rank | Source | What it can tell you | `evidence` when it becomes a viewpoint or an insight | +|---|---|---|---| +| 1 | The project's own data: analytics, support tickets, interview notes, sales calls, the running product | What these people actually do and where they get stuck | insight `source.type` = analytics, interview, ticket, run | +| 2 | Regulations and standards for the field: laws, WCAG, industry rules | What must be true whatever anyone prefers | `author`, with the clause quoted | +| 3 | Published research with a method: Baymard, NN/g studies, peer-reviewed HCI | What holds across many products of this kind, and how strongly | `study` | +| 4 | Public design systems and style guides: GOV.UK, Carbon, Primer, Polaris, USWDS | Patterns that were tested and are maintained, with usage rules | `author` | +| 5 | Products people in the field already use, and their reviews | The conventions they expect and the complaints they repeat | `secondary`, the review quoted | +| 6 | Community practice: articles, talks, threads | Ideas worth testing | `folklore` — never more than a hypothesis | + +The lenses (`lenses/<kind>.md`) and the topic pools behind them are rank 3 and 4, already fetched and +quoted. Read them for the kind of screen; research is for what they cannot know — this field. + +## What to come back with + +1. **The field's words.** The terms people in the field use for its records, states and outcomes, each + with where you saw it. Labels, headings and messages use these words, not the database's. +2. **The rules.** What a regulation or standard requires of this kind of screen (consent, retention, + disclosure, accessibility), quoted with its clause. +3. **The work.** Which task is repeated most, how often, under what pressure, on which device — from + rank 1 sources if the project has them; otherwise it stays an unknown. +4. **The conventions.** How two or three products the people already use handle the same task, and what + their reviews complain about. A convention people already know is a reason, not a rule. +5. **The open questions.** Everything you could not settle, as questions in the actor's `unknowns[]`. + +## How to write it down + +- A finding about people or about this field is an insight (`references/understand.md`): one claim, + its `source`, the exact numbers or words in `evidence[]`, and what would prove it wrong. +- A finding about how screens of this kind should work is a candidate viewpoint: who said it, where, + their words. Bring it to the person running you; uxcli's pools only carry what someone can cite. +- Fetch what you cite in this session and quote it exactly. A page you could not open is not a source: + say so and leave the question open. +- Then design: `uxcli context show <journey>`, the lens for each screen the template names, and the + mockups (`references/mockups.md`). + +## What you do not do + +- Fill an unknown from what you expect the field to be like. An unanswered question stays a question. +- Treat a competitor's screen as evidence that its pattern works. It is evidence that it exists. +- Raise community advice above a hypothesis because several people repeat it. +- Quote from memory. If the words are not in something you fetched, they are not a quote. diff --git a/skills/uxcli/references/versions.md b/skills/uxcli/references/versions.md new file mode 100644 index 0000000..27e2f7a --- /dev/null +++ b/skills/uxcli/references/versions.md @@ -0,0 +1,43 @@ +# versions — proposing a better version, and showing it is better + +Read when: a walk of a journey showed something worth changing, or you are about to change a screen +that a journey goes through. A version names one walk so it is kept; a redesign proposal says what to +change and points at the evidence; the next version is compared with the last, step by step. + +## The loop + +```bash +npx -y @junixlabs/uxcli run .uxcli/journeys/<id>.json # walk it +npx -y @junixlabs/uxcli experience --page # what the person goes through, pinned on screenshots +npx -y @junixlabs/uxcli version save <id> v1 --note="as it is" # name that walk; it is kept against pruning +# write the redesign proposal (below); a person approves it; draw and build the change +npx -y @junixlabs/uxcli run .uxcli/journeys/<id>.json +npx -y @junixlabs/uxcli version save <id> v2 --note="call button above the fold" --proposal=proposals/P-0012.json +npx -y @junixlabs/uxcli experience --journey=<id> --from=v1 --to=v2 --page # both pictures of every step, what moved +``` + +## A redesign proposal + +`.uxcli/proposals/P-xxxx.json` with `"kind": "redesign"`, `target` naming the journey and step +(`journeys/<id>.json#<step>`), `statement` saying what changes for the person, and `evidence[]` citing +what the walk showed — one entry per finding it answers: + +```json +{ "run": "runs/R-…", "check": "experience.reach", "observed": "[data-uxcli=call-action] 2 scrolls away at 390x844", "what": "the call button is below the fold on a phone" } +``` + +A proposal with no evidence is an opinion; cite the walk, the lens viewpoint, or the insight it rests +on. A person approves it before it is built. Name the version that answers it with `--proposal`. + +## Saying it is better + +Report the comparison in its own words: the steps, typing, scrolls and estimate that moved, the +findings that went away and the ones that appeared. A version with fewer findings and a higher +estimate is not simply better; say both. The metrics are findings, not verdicts, and nobody was +watched: a better walk is evidence for the change, not proof that people prefer it. + +## What you do not do + +- Rewrite or delete a version. A new walk gets a new name. +- Save a version of a walk you did not make on the final files. +- Call a change better without the comparison, or show only the numbers that improved. diff --git a/skills/uxcli/templates/landing.json b/skills/uxcli/templates/landing.json new file mode 100644 index 0000000..a24a311 --- /dev/null +++ b/skills/uxcli/templates/landing.json @@ -0,0 +1,84 @@ +{ + "schema_version": 1, + "id": "landing", + "name": "Product site", + "when": "Marketing sites for a product or service: a visitor decides in minutes whether to sign up, buy or get in touch.", + "actors": [ + { + "actor": "evaluating_visitor", + "who": "Someone who arrived with a problem and is deciding whether this product solves it.", + "questions": [ + "What problem were they trying to solve when they arrived, in their own words?", + "Where did they come from (search, a recommendation, an ad, a comparison article)?", + "What do they need to see before they believe it works for someone like them?", + "What stops them from signing up today: price, effort, risk, someone else's approval?", + "Who else is involved in the decision, and what will that person ask?" + ] + }, + { + "actor": "returning_buyer", + "who": "Someone who has seen the site before and comes back to compare plans or to buy.", + "questions": [ + "What did they come back to check?", + "Which plan details decide between two plans for them?", + "What would make them contact sales instead of signing up themselves?" + ] + } + ], + "screens": [ + { "id": "home", "name": "Home", "lens": "marketing", "does": "Understands what this is, who it is for and what to do next." }, + { "id": "feature", "name": "Feature or use-case page", "lens": "marketing", "does": "Sees how the product handles their specific case." }, + { "id": "pricing", "name": "Pricing", "lens": "marketing", "does": "Works out what it will cost them and which plan fits." }, + { "id": "sign-up", "name": "Sign up", "lens": "transaction", "does": "Creates an account with as little as is needed to start." }, + { "id": "contact", "name": "Contact sales", "lens": "transaction", "does": "Asks a question and knows when they will hear back." }, + { "id": "docs", "name": "Docs or help", "lens": "content", "does": "Checks a detail before committing." } + ], + "journeys": [ + { + "id": "understand-and-sign-up", + "goal": "Understand the product and sign up", + "actor": "evaluating_visitor", + "through": ["home", "feature", "sign-up"], + "watch": [ + "What the product is and who it is for is readable without scrolling at a phone viewport.", + "Each view has one primary call to action, and it says what happens when pressed.", + "Count the sign-up fields; each must be needed to start." + ] + }, + { + "id": "compare-plans", + "goal": "Choose a plan", + "actor": "returning_buyer", + "through": ["home", "pricing", "sign-up"], + "watch": [ + "Prices state the period, the currency and whether tax is included.", + "The differences between plans can be read without hovering or opening anything.", + "The plan chosen on pricing is the plan selected in sign-up." + ] + }, + { + "id": "ask-a-question", + "goal": "Ask sales a question", + "actor": "returning_buyer", + "through": ["pricing", "contact"], + "watch": [ + "The contact form asks only what sales needs to reply.", + "After sending, the page says what happens next and when.", + "An error keeps the message the visitor typed." + ] + } + ], + "research": [ + "Which words do people in this market use for the problem, as opposed to the product's own feature names?", + "Which competitors do visitors compare against, and how do those sites explain price and value?", + "What proof does this market trust: customer logos, numbers, reviews, certifications, a free trial?", + "Which regulations apply to sign-up and marketing consent here?", + "What questions do sales or support hear most from people who have not bought yet?" + ], + "sources": [ + { "name": "Nielsen Norman Group — 10 usability heuristics", "url": "https://www.nngroup.com/articles/ten-usability-heuristics/", "why": "The baseline vocabulary for reviewing any screen." }, + { "name": "GOV.UK — content design: writing for GOV.UK", "url": "https://www.gov.uk/guidance/content-design/writing-for-gov-uk", "why": "Researched guidance on plain language and how people read on screens." }, + { "name": "Baymard Institute — research", "url": "https://baymard.com/research", "why": "Research on account creation, form length and trust signals before purchase." }, + { "name": "Deceptive Patterns (Harry Brignull)", "url": "https://www.deceptive.design/", "why": "The catalogue of patterns that win a sign-up and lose the customer." } + ] +} diff --git a/skills/uxcli/templates/landing.md b/skills/uxcli/templates/landing.md new file mode 100644 index 0000000..63f82aa --- /dev/null +++ b/skills/uxcli/templates/landing.md @@ -0,0 +1,83 @@ +# Product site — the `landing` template + +Marketing sites for a product or service: a visitor decides in minutes whether to sign up, buy or get in touch. + +A template is where research starts, not what it found. Nothing below is a fact about this product's users: the screens and journeys are what products of this kind usually have, and every question is yours to answer from a source before it becomes an insight. `uxcli template apply landing` writes the actor questions into `.uxcli/understanding/actors/`; `references/research.md` in this skill is how to answer them. + +## Who uses it + +### `evaluating_visitor` + +Someone who arrived with a problem and is deciding whether this product solves it. + +- What problem were they trying to solve when they arrived, in their own words? +- Where did they come from (search, a recommendation, an ad, a comparison article)? +- What do they need to see before they believe it works for someone like them? +- What stops them from signing up today: price, effort, risk, someone else's approval? +- Who else is involved in the decision, and what will that person ask? + +### `returning_buyer` + +Someone who has seen the site before and comes back to compare plans or to buy. + +- What did they come back to check? +- Which plan details decide between two plans for them? +- What would make them contact sales instead of signing up themselves? + +## Screens, and the lens each is read against + +| Screen | Lens | What the person does there | +|---|---|---| +| Home (`home`) | `marketing` | Understands what this is, who it is for and what to do next. | +| Feature or use-case page (`feature`) | `marketing` | Sees how the product handles their specific case. | +| Pricing (`pricing`) | `marketing` | Works out what it will cost them and which plan fits. | +| Sign up (`sign-up`) | `transaction` | Creates an account with as little as is needed to start. | +| Contact sales (`contact`) | `transaction` | Asks a question and knows when they will hear back. | +| Docs or help (`docs`) | `content` | Checks a detail before committing. | + +## Journeys to walk first + +### `understand-and-sign-up` — Understand the product and sign up + +Actor `evaluating_visitor`, through: `home` → `feature` → `sign-up`. + +Watch when you walk it: + +- What the product is and who it is for is readable without scrolling at a phone viewport. +- Each view has one primary call to action, and it says what happens when pressed. +- Count the sign-up fields; each must be needed to start. + +### `compare-plans` — Choose a plan + +Actor `returning_buyer`, through: `home` → `pricing` → `sign-up`. + +Watch when you walk it: + +- Prices state the period, the currency and whether tax is included. +- The differences between plans can be read without hovering or opening anything. +- The plan chosen on pricing is the plan selected in sign-up. + +### `ask-a-question` — Ask sales a question + +Actor `returning_buyer`, through: `pricing` → `contact`. + +Watch when you walk it: + +- The contact form asks only what sales needs to reply. +- After sending, the page says what happens next and when. +- An error keeps the message the visitor typed. + +## What to research about the domain + +- Which words do people in this market use for the problem, as opposed to the product's own feature names? +- Which competitors do visitors compare against, and how do those sites explain price and value? +- What proof does this market trust: customer logos, numbers, reviews, certifications, a free trial? +- Which regulations apply to sign-up and marketing consent here? +- What questions do sales or support hear most from people who have not bought yet? + +## Where research starts + +- [Nielsen Norman Group — 10 usability heuristics](https://www.nngroup.com/articles/ten-usability-heuristics/) — The baseline vocabulary for reviewing any screen. +- [GOV.UK — content design: writing for GOV.UK](https://www.gov.uk/guidance/content-design/writing-for-gov-uk) — Researched guidance on plain language and how people read on screens. +- [Baymard Institute — research](https://baymard.com/research) — Research on account creation, form length and trust signals before purchase. +- [Deceptive Patterns (Harry Brignull)](https://www.deceptive.design/) — The catalogue of patterns that win a sign-up and lose the customer. diff --git a/skills/uxcli/templates/shop.json b/skills/uxcli/templates/shop.json new file mode 100644 index 0000000..618f1df --- /dev/null +++ b/skills/uxcli/templates/shop.json @@ -0,0 +1,86 @@ +{ + "schema_version": 1, + "id": "shop", + "name": "Online shop", + "when": "E-commerce and marketplaces: people browse, compare, buy, and come back for their order.", + "actors": [ + { + "actor": "first_time_shopper", + "who": "Someone who arrived from search or an ad and has never bought here.", + "questions": [ + "What were they looking for when they arrived, in their own words?", + "What do they need to know before trusting this shop with a payment?", + "Which device do they buy on, and do they start on one device and finish on another?", + "What costs or conditions would make them abandon at checkout (delivery price, delivery date, returns)?", + "How do they compare products: by price, by a spec, by reviews, by photos?" + ] + }, + { + "actor": "returning_customer", + "who": "Someone who has bought before and comes back to buy again or to check an order.", + "questions": [ + "Why do they come back: to reorder, to track, to return?", + "Do they expect to be signed in, and what happens if they are not?", + "What would they need to see to resolve an order problem without contacting support?" + ] + } + ], + "screens": [ + { "id": "catalogue", "name": "Catalogue or category", "lens": "shop", "does": "Browses a range and narrows it down." }, + { "id": "search-results", "name": "Search results", "lens": "shop", "does": "Finds the product they have in mind, or a close alternative." }, + { "id": "product", "name": "Product page", "lens": "shop", "does": "Decides whether this is the right product at the right total price and delivery date." }, + { "id": "cart", "name": "Cart", "lens": "transaction", "does": "Checks what they are buying and what it will cost in total." }, + { "id": "checkout", "name": "Checkout", "lens": "transaction", "does": "Gives delivery and payment details once and places the order." }, + { "id": "confirmation", "name": "Order confirmation", "lens": "transaction", "does": "Knows the order went through and what happens next." }, + { "id": "orders", "name": "Order history", "lens": "workspace", "does": "Finds an order, sees where it is, and starts a return." } + ], + "journeys": [ + { + "id": "find-and-buy", + "goal": "Find a product and buy it", + "actor": "first_time_shopper", + "through": ["search-results", "product", "cart", "checkout", "confirmation"], + "watch": [ + "The total cost, including delivery, is visible before the last step.", + "Checkout can be finished without creating an account.", + "Count the form fields in checkout; each must be needed to deliver or to charge.", + "An error in checkout keeps everything the shopper already typed." + ] + }, + { + "id": "compare-and-choose", + "goal": "Narrow a range down to one product", + "actor": "first_time_shopper", + "through": ["catalogue", "product", "catalogue", "product"], + "watch": [ + "Filters apply without losing scroll position, and say how many results remain.", + "Going back from a product returns to the same place in the list.", + "The facts people compare on are in the same place on every product page." + ] + }, + { + "id": "track-and-return", + "goal": "Find a past order and start a return", + "actor": "returning_customer", + "through": ["orders", "confirmation"], + "watch": [ + "The order's current status and expected date are visible without opening a sub-page.", + "The return starts from the order, not from a help article.", + "Every step says what happens next and when." + ] + } + ], + "research": [ + "What do shoppers in this category need to see before buying (sizes, compatibility, ingredients, warranty)?", + "Which delivery, payment and return options are expected in this market, and which are legally required?", + "What do customer reviews of this shop and its competitors complain about most?", + "What share of traffic is on phones, and where does it come from (search, social, email)?", + "Which local conventions matter: currency format, address format, phone format, tax shown or not?" + ], + "sources": [ + { "name": "Baymard Institute — e-commerce UX research", "url": "https://baymard.com/research", "why": "Large-sample usability research on product pages, search, cart and checkout." }, + { "name": "Nielsen Norman Group — 10 usability heuristics", "url": "https://www.nngroup.com/articles/ten-usability-heuristics/", "why": "The baseline vocabulary for reviewing any screen." }, + { "name": "Shopify Polaris", "url": "https://polaris.shopify.com/", "why": "Commerce patterns and content guidelines from a platform that runs many shops." }, + { "name": "GOV.UK Design System — patterns", "url": "https://design-system.service.gov.uk/patterns/", "why": "Researched patterns for addresses, payment details, errors and confirmation pages." } + ] +} diff --git a/skills/uxcli/templates/shop.md b/skills/uxcli/templates/shop.md new file mode 100644 index 0000000..ba464f1 --- /dev/null +++ b/skills/uxcli/templates/shop.md @@ -0,0 +1,85 @@ +# Online shop — the `shop` template + +E-commerce and marketplaces: people browse, compare, buy, and come back for their order. + +A template is where research starts, not what it found. Nothing below is a fact about this product's users: the screens and journeys are what products of this kind usually have, and every question is yours to answer from a source before it becomes an insight. `uxcli template apply shop` writes the actor questions into `.uxcli/understanding/actors/`; `references/research.md` in this skill is how to answer them. + +## Who uses it + +### `first_time_shopper` + +Someone who arrived from search or an ad and has never bought here. + +- What were they looking for when they arrived, in their own words? +- What do they need to know before trusting this shop with a payment? +- Which device do they buy on, and do they start on one device and finish on another? +- What costs or conditions would make them abandon at checkout (delivery price, delivery date, returns)? +- How do they compare products: by price, by a spec, by reviews, by photos? + +### `returning_customer` + +Someone who has bought before and comes back to buy again or to check an order. + +- Why do they come back: to reorder, to track, to return? +- Do they expect to be signed in, and what happens if they are not? +- What would they need to see to resolve an order problem without contacting support? + +## Screens, and the lens each is read against + +| Screen | Lens | What the person does there | +|---|---|---| +| Catalogue or category (`catalogue`) | `shop` | Browses a range and narrows it down. | +| Search results (`search-results`) | `shop` | Finds the product they have in mind, or a close alternative. | +| Product page (`product`) | `shop` | Decides whether this is the right product at the right total price and delivery date. | +| Cart (`cart`) | `transaction` | Checks what they are buying and what it will cost in total. | +| Checkout (`checkout`) | `transaction` | Gives delivery and payment details once and places the order. | +| Order confirmation (`confirmation`) | `transaction` | Knows the order went through and what happens next. | +| Order history (`orders`) | `workspace` | Finds an order, sees where it is, and starts a return. | + +## Journeys to walk first + +### `find-and-buy` — Find a product and buy it + +Actor `first_time_shopper`, through: `search-results` → `product` → `cart` → `checkout` → `confirmation`. + +Watch when you walk it: + +- The total cost, including delivery, is visible before the last step. +- Checkout can be finished without creating an account. +- Count the form fields in checkout; each must be needed to deliver or to charge. +- An error in checkout keeps everything the shopper already typed. + +### `compare-and-choose` — Narrow a range down to one product + +Actor `first_time_shopper`, through: `catalogue` → `product` → `catalogue` → `product`. + +Watch when you walk it: + +- Filters apply without losing scroll position, and say how many results remain. +- Going back from a product returns to the same place in the list. +- The facts people compare on are in the same place on every product page. + +### `track-and-return` — Find a past order and start a return + +Actor `returning_customer`, through: `orders` → `confirmation`. + +Watch when you walk it: + +- The order's current status and expected date are visible without opening a sub-page. +- The return starts from the order, not from a help article. +- Every step says what happens next and when. + +## What to research about the domain + +- What do shoppers in this category need to see before buying (sizes, compatibility, ingredients, warranty)? +- Which delivery, payment and return options are expected in this market, and which are legally required? +- What do customer reviews of this shop and its competitors complain about most? +- What share of traffic is on phones, and where does it come from (search, social, email)? +- Which local conventions matter: currency format, address format, phone format, tax shown or not? + +## Where research starts + +- [Baymard Institute — e-commerce UX research](https://baymard.com/research) — Large-sample usability research on product pages, search, cart and checkout. +- [Nielsen Norman Group — 10 usability heuristics](https://www.nngroup.com/articles/ten-usability-heuristics/) — The baseline vocabulary for reviewing any screen. +- [Shopify Polaris](https://polaris.shopify.com/) — Commerce patterns and content guidelines from a platform that runs many shops. +- [GOV.UK Design System — patterns](https://design-system.service.gov.uk/patterns/) — Researched patterns for addresses, payment details, errors and confirmation pages. diff --git a/skills/uxcli/templates/workspace.json b/skills/uxcli/templates/workspace.json new file mode 100644 index 0000000..c435c71 --- /dev/null +++ b/skills/uxcli/templates/workspace.json @@ -0,0 +1,97 @@ +{ + "schema_version": 1, + "id": "workspace", + "name": "B2B workspace", + "when": "CRMs, admin panels, project and ticket tools, internal back-offices: people who come back every day to work through records.", + "actors": [ + { + "actor": "daily_operator", + "who": "The person who works through records all day: a sales rep, a support agent, a coordinator.", + "questions": [ + "Which single task do they repeat most in a day, and how many times?", + "How many records do they handle per day, and how many are open at once?", + "On which device and where do they do it: a desk, a phone between meetings, a site visit?", + "What words do they use for the records and states, as opposed to the words the database uses?", + "What do they switch to outside the product to finish a task (phone, email, chat, spreadsheet)?", + "What makes them lose their place: an interruption, a page reload, a filter that resets?" + ] + }, + { + "actor": "team_admin", + "who": "The person who sets the product up for others: permissions, fields, imports, reports.", + "questions": [ + "Which settings do they change after the first week, and how often?", + "Who do they answer to for what the team sees in reports?", + "What happens today when a permission or a field is set wrong, and who notices?" + ] + } + ], + "screens": [ + { "id": "sign-in", "name": "Sign in", "lens": "transaction", "does": "Gets into their own workspace and lands where they left off." }, + { "id": "first-run", "name": "First run (empty)", "lens": "workspace", "does": "Understands what goes here and creates or imports the first record." }, + { "id": "record-list", "name": "Record list", "lens": "workspace", "does": "Finds the record they need by searching, filtering or sorting, and opens it." }, + { "id": "record-detail", "name": "Record detail", "lens": "workspace", "does": "Reads the record and takes the next action on it without leaving the screen." }, + { "id": "record-form", "name": "Create or edit record", "lens": "transaction", "does": "Enters or corrects the record's data once, with errors caught before they save." }, + { "id": "overview", "name": "Overview dashboard", "lens": "data", "does": "Sees what needs attention today and goes straight to it." }, + { "id": "settings", "name": "Settings", "lens": "workspace", "does": "Changes how the workspace behaves and sees that the change took effect." } + ], + "journeys": [ + { + "id": "first-value", + "goal": "Get from sign-up to the first useful record", + "actor": "daily_operator", + "through": ["sign-in", "first-run", "record-form", "record-list"], + "watch": [ + "The empty state says what goes here and offers the one action that starts it.", + "Count the steps from landing to the first saved record; every step must earn its place.", + "After saving, the new record is visible where the person expects it, without a reload." + ] + }, + { + "id": "find-and-act", + "goal": "Find one record and take the next action on it", + "actor": "daily_operator", + "through": ["record-list", "record-detail"], + "watch": [ + "The primary action on the detail screen is reachable without scrolling at a phone viewport.", + "Every action shows a change within a second; anything longer shows progress.", + "Going back to the list keeps the search, filter and scroll position." + ] + }, + { + "id": "triage-today", + "goal": "See what needs attention today and clear it", + "actor": "daily_operator", + "through": ["overview", "record-detail", "overview"], + "watch": [ + "Numbers on the overview link to the records behind them.", + "After acting on a record, the overview reflects the change when the person returns.", + "Nothing on the overview needs a legend the screen does not show." + ] + }, + { + "id": "change-a-setting", + "goal": "Change one setting and confirm it applied", + "actor": "team_admin", + "through": ["settings", "record-list"], + "watch": [ + "The setting says what it changes before it is saved.", + "A destructive or team-wide change names its consequence and can be undone or is confirmed.", + "The effect is visible on the screen it changes." + ] + } + ], + "research": [ + "What is the domain's own vocabulary for records, stages and outcomes, and where is it written down?", + "Which regulations or audit rules apply to the data (personal data, retention, who may see what)?", + "Which competing or adjacent tools do these teams already use, and what do people praise or complain about in their reviews?", + "What volume and speed does the work have: records per day, time allowed per record, peak hours?", + "What does a mistake cost in this domain: a lost customer, a fine, a re-do?" + ], + "sources": [ + { "name": "Nielsen Norman Group — 10 usability heuristics", "url": "https://www.nngroup.com/articles/ten-usability-heuristics/", "why": "The baseline vocabulary for reviewing any workspace screen." }, + { "name": "IBM Carbon Design System", "url": "https://carbondesignsystem.com/", "why": "Enterprise patterns for data tables, forms, notifications and empty states, documented with usage guidance." }, + { "name": "GitHub Primer", "url": "https://primer.style/", "why": "Patterns from a tool people work in all day: navigation, lists, empty states, saving." }, + { "name": "GOV.UK Design System — patterns", "url": "https://design-system.service.gov.uk/patterns/", "why": "Researched patterns for questions, errors and confirmation, tested with real users." } + ] +} diff --git a/skills/uxcli/templates/workspace.md b/skills/uxcli/templates/workspace.md new file mode 100644 index 0000000..f7d1513 --- /dev/null +++ b/skills/uxcli/templates/workspace.md @@ -0,0 +1,95 @@ +# B2B workspace — the `workspace` template + +CRMs, admin panels, project and ticket tools, internal back-offices: people who come back every day to work through records. + +A template is where research starts, not what it found. Nothing below is a fact about this product's users: the screens and journeys are what products of this kind usually have, and every question is yours to answer from a source before it becomes an insight. `uxcli template apply workspace` writes the actor questions into `.uxcli/understanding/actors/`; `references/research.md` in this skill is how to answer them. + +## Who uses it + +### `daily_operator` + +The person who works through records all day: a sales rep, a support agent, a coordinator. + +- Which single task do they repeat most in a day, and how many times? +- How many records do they handle per day, and how many are open at once? +- On which device and where do they do it: a desk, a phone between meetings, a site visit? +- What words do they use for the records and states, as opposed to the words the database uses? +- What do they switch to outside the product to finish a task (phone, email, chat, spreadsheet)? +- What makes them lose their place: an interruption, a page reload, a filter that resets? + +### `team_admin` + +The person who sets the product up for others: permissions, fields, imports, reports. + +- Which settings do they change after the first week, and how often? +- Who do they answer to for what the team sees in reports? +- What happens today when a permission or a field is set wrong, and who notices? + +## Screens, and the lens each is read against + +| Screen | Lens | What the person does there | +|---|---|---| +| Sign in (`sign-in`) | `transaction` | Gets into their own workspace and lands where they left off. | +| First run (empty) (`first-run`) | `workspace` | Understands what goes here and creates or imports the first record. | +| Record list (`record-list`) | `workspace` | Finds the record they need by searching, filtering or sorting, and opens it. | +| Record detail (`record-detail`) | `workspace` | Reads the record and takes the next action on it without leaving the screen. | +| Create or edit record (`record-form`) | `transaction` | Enters or corrects the record's data once, with errors caught before they save. | +| Overview dashboard (`overview`) | `data` | Sees what needs attention today and goes straight to it. | +| Settings (`settings`) | `workspace` | Changes how the workspace behaves and sees that the change took effect. | + +## Journeys to walk first + +### `first-value` — Get from sign-up to the first useful record + +Actor `daily_operator`, through: `sign-in` → `first-run` → `record-form` → `record-list`. + +Watch when you walk it: + +- The empty state says what goes here and offers the one action that starts it. +- Count the steps from landing to the first saved record; every step must earn its place. +- After saving, the new record is visible where the person expects it, without a reload. + +### `find-and-act` — Find one record and take the next action on it + +Actor `daily_operator`, through: `record-list` → `record-detail`. + +Watch when you walk it: + +- The primary action on the detail screen is reachable without scrolling at a phone viewport. +- Every action shows a change within a second; anything longer shows progress. +- Going back to the list keeps the search, filter and scroll position. + +### `triage-today` — See what needs attention today and clear it + +Actor `daily_operator`, through: `overview` → `record-detail` → `overview`. + +Watch when you walk it: + +- Numbers on the overview link to the records behind them. +- After acting on a record, the overview reflects the change when the person returns. +- Nothing on the overview needs a legend the screen does not show. + +### `change-a-setting` — Change one setting and confirm it applied + +Actor `team_admin`, through: `settings` → `record-list`. + +Watch when you walk it: + +- The setting says what it changes before it is saved. +- A destructive or team-wide change names its consequence and can be undone or is confirmed. +- The effect is visible on the screen it changes. + +## What to research about the domain + +- What is the domain's own vocabulary for records, stages and outcomes, and where is it written down? +- Which regulations or audit rules apply to the data (personal data, retention, who may see what)? +- Which competing or adjacent tools do these teams already use, and what do people praise or complain about in their reviews? +- What volume and speed does the work have: records per day, time allowed per record, peak hours? +- What does a mistake cost in this domain: a lost customer, a fine, a re-do? + +## Where research starts + +- [Nielsen Norman Group — 10 usability heuristics](https://www.nngroup.com/articles/ten-usability-heuristics/) — The baseline vocabulary for reviewing any workspace screen. +- [IBM Carbon Design System](https://carbondesignsystem.com/) — Enterprise patterns for data tables, forms, notifications and empty states, documented with usage guidance. +- [GitHub Primer](https://primer.style/) — Patterns from a tool people work in all day: navigation, lists, empty states, saving. +- [GOV.UK Design System — patterns](https://design-system.service.gov.uk/patterns/) — Researched patterns for questions, errors and confirmation, tested with real users. diff --git a/src/adapters/chrome/observer.js b/src/adapters/chrome/observer.js index c28c0d9..fc80997 100644 --- a/src/adapters/chrome/observer.js +++ b/src/adapters/chrome/observer.js @@ -78,7 +78,7 @@ const DOM = `(sels) => { const out = {}; for (const sel of sels) { let el = null; try { el = document.querySelector(sel); } catch {} - if (!el) { out[sel] = { present: false, visible: false, enabled: false, inViewportWithoutScroll: false, scrollsNeeded: null, text: null, value: null }; continue; } + if (!el) { out[sel] = { present: false, visible: false, enabled: false, inViewportWithoutScroll: false, scrollsNeeded: null, text: null, value: null, inputType: null }; continue; } const r = el.getBoundingClientRect(); const H = window.innerHeight, W = window.innerWidth; const inView = r.top >= 0 && r.left >= 0 && r.bottom <= H && r.right <= W; const scrollsNeeded = inView ? 0 : Math.ceil(Math.max(r.bottom - H, -r.top, 0) / H) || (r.right > W || r.left < 0 ? 1 : 0); @@ -87,7 +87,7 @@ const DOM = `(sels) => { // Where the element sits in the viewport at this moment, in CSS pixels: the same frame the // screenshot shows, so a picture can point at the hotspot — or at where it is, below the fold. rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height), viewport: { w: W, h: H } }, - value: 'value' in el && el.tagName !== 'LI' ? String(el.value ?? '') : null }; + value: 'value' in el && el.tagName !== 'LI' ? String(el.value ?? '') : null, inputType: el.tagName === 'INPUT' ? (el.type || 'text') : null }; } return out; }`; @@ -96,6 +96,12 @@ const ALERTS = `() => [...document.querySelectorAll('[role=alert],[role=status], return { role: el.getAttribute('role') || 'live:' + el.getAttribute('aria-live'), text: (el.innerText ?? el.textContent ?? '').trim(), visible: !!el.getClientRects().length && cs.visibility !== 'hidden' && cs.display !== 'none' && parseFloat(cs.opacity) > 0 }; })`; +// Navigation landmarks as a person reads them: each nav's label and the text of its links and buttons, +// in order. Read so a journey's screens can be compared with each other, never to judge one alone. +const NAV = `() => [...document.querySelectorAll('nav,[role=navigation]')].filter(n => n.getClientRects().length).map(n => ({ + label: n.getAttribute('aria-label') || (n.getAttribute('aria-labelledby') && document.getElementById(n.getAttribute('aria-labelledby'))?.textContent?.trim()) || null, + items: [...n.querySelectorAll('a,button,[role=link],[role=menuitem]')].filter(x => x.getClientRects().length).map(x => (x.innerText ?? x.textContent ?? '').trim()).filter(Boolean), +}))`; const STORAGE = `(keys) => { const c = Object.fromEntries(document.cookie.split(';').map(x => x.trim().split('=')).filter(x => x[0]).map(([k, ...v]) => [k, v.join('=')])); const out = {}; for (const k of keys) { const v = localStorage.getItem(k) ?? sessionStorage.getItem(k) ?? c[k] ?? null; out[k] = v === null ? { present: false } : { present: true, value: v }; } @@ -130,6 +136,7 @@ export async function observe(page, opts = {}) { const href = page.url(); const dom = await page.evaluate(new Function('return ' + DOM)(), selectors).catch(() => ({})); const alerts = await page.evaluate(new Function('return ' + ALERTS)()).catch(() => []); + const nav = await page.evaluate(new Function('return ' + NAV)()).catch(() => []); const raw = await page.evaluate(new Function('return ' + STORAGE)(), storageKeys).catch(() => ({})); const storage = {}; for (const [k, v] of Object.entries(raw)) storage[k] = v.present ? { present: true, hash: sha1_8(v.value) } : { present: false }; @@ -155,5 +162,5 @@ export async function observe(page, opts = {}) { const file = `${shotName}.png`; await page.screenshot({ path: path.join(shotDir, file), timeout: 5000 }).then(() => shots.push(file)).catch(() => {}); } - return { at, url: { href, path: pathOf(href) }, dom, a11y: { alerts }, network, storage, timing: { toStable }, shots }; + return { at, url: { href, path: pathOf(href) }, dom, a11y: { alerts }, nav, network, storage, timing: { toStable }, shots }; } diff --git a/src/core/experience.js b/src/core/experience.js new file mode 100644 index 0000000..c5918a8 --- /dev/null +++ b/src/core/experience.js @@ -0,0 +1,144 @@ +// What a person goes through on a journey, read from the trace of one walk in Chrome. Not whether the +// page is well built — whether the person can do what they came to do, how much it asks of them, and +// where it makes them wait, scroll, or say the same thing twice. +// +// Every number here comes from the run packet (run.json): the steps the runner took, the interactions +// it recorded, the time each step took to settle. Every finding is method-unproven, so it is reported as +// a finding with the source of its threshold, never as a fail. Pure: a packet in, a report out. +// +// in: a schema-2 run packet { journey, viewport, steps: [{ id, workflow, action, before, after, +// interactions: [{ type, target, scrollsNeeded, rect, typed: { chars, value } , to, request, status, ms }], +// timing: { toStable }, shots }] } +// out: { journey, viewport, steps: [...per step], totals, findings: [{ metric, step, what, source, shot, rect }] } + +// Keystroke-Level Model operators, in seconds (Card, Moran & Newell, 1980; 1983). K is the average +// skilled typist; a click is pointing then pressing and releasing the button (P + BB). +export const KLM = { K: 0.2, P: 1.1, B: 0.1, H: 0.4, M: 1.35 }; +const SRC = { + klm: 'Card, Moran & Newell, The Keystroke-Level Model for User Performance Time with Interactive Systems (1980)', + response: 'Jakob Nielsen, Response Times: The 3 Important Limits (1993) — 0.1 s, 1 s, 10 s', + reach: 'Steve Krug, Don\'t Make Me Think; NN/g on content below the fold — what a step needs is where the person already is', + twice: 'WCAG 2.2 SC 3.3.7 Redundant Entry — information entered before is not asked for again in the same process', + reached: 'the project\'s own journey: the state this step must reach', + kept: 'GOV.UK Design System, Error message — "Do not clear any form fields … Keep both passing and failing answers." (forms.keep-answers-after-an-error)', + announced: 'WCAG 2.2 SC 3.3.1 Error Identification and 4.1.3 Status Messages — an error is described to the person in text and announced', + consistent: 'WCAG 2.2 SC 3.2.3 Consistent Navigation — navigation repeated on several pages occurs in the same relative order', +}; +export const LIMITS = { instant: 100, flow: 1000, attention: 10000 }; + +const band = ms => ms == null ? null : ms <= LIMITS.instant ? 'instant' : ms <= LIMITS.flow ? 'flow' : ms <= LIMITS.attention ? 'wait' : 'lost'; +const round = n => Math.round(n * 10) / 10; + +// One step's estimate. A target that is typed into: think, point, click, home to the keyboard, type, +// home back. A target that is clicked: think, point, click. Each scroll the target needs is counted as +// one more pointing operator — an approximation the original model does not define, and said so. +function klmOf(ui) { + let s = 0; let typedUnknown = false; + for (const x of ui) { + s += KLM.M + KLM.P + 2 * KLM.B + (x.scrollsNeeded || 0) * KLM.P; + if (x.typed) s += 2 * KLM.H + x.typed.chars * KLM.K; + else if (x.fill) typedUnknown = true; + } + return { seconds: s, typedUnknown }; +} + +// The journey a packet walked: its id, or the file its ref names. +export const journeyName = run => { const j = run?.journey; if (!j) return null; if (typeof j === 'string') return j; return j.id || (j.ref ? String(j.ref).split('/').pop().replace(/\.json$/, '') : null); }; + +export function experience(run) { + const steps = []; const findings = []; + const typedAt = new Map(); // hash of a typed value → the first step it was typed at + const visited = new Map(); // a path navigated to → the first step that went there + const navAt = new Map(); // a navigation landmark → the first step it was seen at, and its items + // A fixture step sets data up before the person arrives; it is not something the person goes through. + for (const st of (run?.steps || []).filter(x => x.kind !== 'fixture')) { + const ui = (st.interactions || []).filter(x => x.type === 'ui'); + const typed = ui.filter(x => x.typed); + const clicks = ui.length - typed.length; + const chars = typed.reduce((n, x) => n + x.typed.chars, 0); + const scrolls = ui.reduce((n, x) => n + (x.scrollsNeeded || 0), 0); + const settle = st.timing?.toStable ?? null; + const api = (st.interactions || []).filter(x => x.type === 'api' && typeof x.ms === 'number'); + const k = klmOf(ui); + const seconds = k.seconds + (settle != null ? settle / 1000 : 0); + const shotBefore = (st.shots || []).find(f => f.includes('-before')) || st.shots?.[0] || null; + const shotAfter = (st.shots || []).find(f => f.includes('-after')) || st.shots?.[st.shots.length - 1] || null; + const row = { id: st.id, workflow: st.workflow || null, action: st.action || null, clicks, fields: typed.length, chars, scrolls, + settleMs: settle, response: band(settle), apiMs: api.reduce((n, x) => n + x.ms, 0), klmSeconds: round(seconds), reached: st.after?.held ?? null, + shots: { before: shotBefore, after: shotAfter } }; + steps.push(row); + + if (settle != null && settle > LIMITS.flow) findings.push({ metric: 'response', step: st.id, workflow: st.workflow || null, + what: `${st.action || st.id}: ${(settle / 1000).toFixed(1)} s before the screen settles${settle > LIMITS.attention ? ' — past 10 s attention is lost' : ' — past 1 s the person needs a sign that something is happening'}`, + source: SRC.response, shot: shotAfter }); + for (const x of ui) if ((x.scrollsNeeded || 0) > 0) findings.push({ metric: 'reach', step: st.id, workflow: st.workflow || null, + what: `${x.target} is ${x.scrollsNeeded} scroll${x.scrollsNeeded === 1 ? '' : 's'} away at ${x.viewport || run.viewport || 'this viewport'} when this step needs it`, + source: SRC.reach, shot: shotBefore, ...(x.rect && { rect: x.rect }) }); + for (const x of typed) { + const first = typedAt.get(x.typed.value); + if (first && first.step !== st.id && first.workflow === (st.workflow || null)) findings.push({ metric: 'asked-twice', step: st.id, workflow: st.workflow || null, + what: `the answer typed into ${x.target} was already typed at ${first.step} (${first.target})`, + source: SRC.twice, shot: shotBefore, ...(x.rect && { rect: x.rect }) }); + else if (!first) typedAt.set(x.typed.value, { step: st.id, target: x.target, workflow: st.workflow || null }); + } + if (st.after?.held === false) findings.push({ metric: 'not-reached', step: st.id, workflow: st.workflow || null, + what: `${st.action || st.id} did not reach ${st.after.state}${st.after.what ? `: ${st.after.what}` : ''}`, source: SRC.reached, shot: shotAfter }); + // Recovery: a step the server answered with an error is where answers get lost and messages go missing. + const errored = (st.interactions || []).some(i => i.type === 'api' && (i.status >= 400 || i.blocked)); + if (errored) { + row.error = true; + // A password is cleared after a failed sign-in on purpose: the security exception GOV.UK and the + // platforms make, so it is not counted. + for (const x of typed) if (x.typed.keptAfter === false && !x.typed.secret) findings.push({ metric: 'recovery', step: st.id, workflow: st.workflow || null, + what: `after the error, ${x.target} no longer holds what the person typed — they have to type it again`, source: SRC.kept, shot: shotAfter, ...(x.rect && { rect: x.rect }) }); + if (Array.isArray(st.announced) && !st.announced.length) findings.push({ metric: 'recovery', step: st.id, workflow: st.workflow || null, + what: `${st.action || st.id}: the server answered with an error and nothing on the page announced it (no visible role=alert, role=status or live region with text)`, source: SRC.announced, shot: shotAfter }); + } + // Consistency: the same navigation, in the same order, on every screen of the workflow that shows it. + for (const [i, n] of (st.nav || []).entries()) { + const key = `${st.workflow}|${n.label || `nav ${i + 1}`}`; + const seen = navAt.get(key); + if (!seen) { navAt.set(key, { step: st.id, items: n.items }); continue; } + const common = n.items.filter(t => seen.items.includes(t)); + const order = seen.items.filter(t => common.includes(t)); + if (common.length > 1 && common.join('\u0000') !== order.join('\u0000')) findings.push({ metric: 'consistency', step: st.id, workflow: st.workflow || null, + what: `${n.label ? `the "${n.label}" navigation` : `navigation ${i + 1}`} reads ${common.join(' · ')} here and ${order.join(' · ')} at ${seen.step}`, source: SRC.consistent, shot: shotBefore }); + } + for (const x of (st.interactions || []).filter(i => i.type === 'navigation' && i.to)) { + const was = visited.get(`${st.workflow}|${x.to}`); + if (was && was !== st.id) row.revisits = [...(row.revisits || []), { to: x.to, first: was }]; + else visited.set(`${st.workflow}|${x.to}`, st.id); + } + if (k.typedUnknown) row.typedUnknown = true; + } + // A journey's workflows are alternatives — the happy path, a wrong password, a server error — and a + // person goes through one of them, so each is totalled on its own. The first is the one the runner + // walked first, the happy path; `totals` is that one's. + const totalsOf = xs => ({ + steps: xs.length, + clicks: xs.reduce((n, s) => n + s.clicks, 0), + fields: xs.reduce((n, s) => n + s.fields, 0), + chars: xs.reduce((n, s) => n + s.chars, 0), + scrolls: xs.reduce((n, s) => n + s.scrolls, 0), + klmSeconds: round(xs.reduce((n, s) => n + s.klmSeconds, 0)), + slowestMs: xs.reduce((m, s) => Math.max(m, s.settleMs ?? 0), 0), + reached: xs.filter(s => s.reached === true).length, + }); + const ids = [...new Set(steps.map(s => s.workflow))]; + const workflows = ids.map(id => ({ id, totals: totalsOf(steps.filter(s => s.workflow === id)) })); + const totals = workflows[0]?.totals || totalsOf([]); + const viewport = run?.viewport || (run?.steps || []).flatMap(x => x.interactions || []).find(x => x.viewport)?.viewport || null; + return { journey: journeyName(run), viewport, ranAt: run?.ranAt || null, steps, workflows, totals, findings, sources: { klm: SRC.klm, response: SRC.response } }; +} + +// Two walks of the same journey, side by side: what moved, by how much. Totals are the first +// workflow's on each side; steps and findings are matched within their workflow. +export function compareExperience(a, b) { + const keys = ['steps', 'clicks', 'fields', 'chars', 'scrolls', 'klmSeconds', 'slowestMs']; + const totals = Object.fromEntries(keys.map(k => [k, { a: a.totals[k], b: b.totals[k], delta: round((b.totals[k] ?? 0) - (a.totals[k] ?? 0)) }])); + const sk = s => `${s.workflow}/${s.id}`; + const ids = [...new Set([...a.steps.map(sk), ...b.steps.map(sk)])]; + const steps = ids.map(id => { const x = a.steps.find(s => sk(s) === id); const y = b.steps.find(s => sk(s) === id); return { id, a: x?.klmSeconds ?? null, b: y?.klmSeconds ?? null, only: !x ? 'b' : !y ? 'a' : null }; }); + const key = f => `${f.metric}|${f.workflow}|${f.step}|${f.what}`; + return { totals, steps, fixed: a.findings.filter(f => !b.findings.some(g => key(g) === key(f))), introduced: b.findings.filter(f => !a.findings.some(g => key(g) === key(f))) }; +} diff --git a/src/core/level/projection.js b/src/core/level/projection.js index 3496517..f47384e 100644 --- a/src/core/level/projection.js +++ b/src/core/level/projection.js @@ -16,9 +16,9 @@ export function insightStanding(i) { if (i.demotedAt || check?.fired === true) { return { confidence: 'hypothesis', demoted: day(i.demotedAt || check?.at), propagated: (i.propagatedTo || []).map(idOf) }; } - if (!evidence) return { confidence: 'hypothesis', why: 'không evidence' }; - if (!i.wouldChangeIf) return { confidence: 'low', why: 'không wouldChangeIf' }; - if (!check) return { confidence: 'medium', why: 'wouldChangeIf chưa từng kiểm' }; + if (!evidence) return { confidence: 'hypothesis', why: 'no evidence' }; + if (!i.wouldChangeIf) return { confidence: 'low', why: 'no wouldChangeIf' }; + if (!check) return { confidence: 'medium', why: 'wouldChangeIf never checked' }; return { confidence: 'high', lastCheck: day(check.at), fired: false }; } @@ -68,24 +68,24 @@ export function projection({ insights: insightList = [], journeys = [], commitme const corpusN = corpusLabels.length; const failedProbes = new Set(completed.flatMap(r => (r.probes || []).filter(p => ['fail', 'finding'].includes(p.value)).map(p => p.id))); for (const id of failedProbes) { - if (!fpMeasured(probes.find(p => p.id === id), corpusLabels, id)) toGate.push(`probe ${id}: fpRate chưa đo (${corpusN} corpus case, cần ≥ N) → chưa được fail`); + if (!fpMeasured(probes.find(p => p.id === id), corpusLabels, id)) toGate.push(`probe ${id}: fpRate not measured (${corpusN} corpus case${corpusN === 1 ? '' : 's'}, needs ≥ N) → may not fail yet`); } for (const c of commitments) { const n = unproven(c), total = (c.measurements || []).length; if (c.status === 'ACTIVE' && n) { - const label = n === total ? (total === 1 ? 'measurement' : total === 2 ? 'cả hai measurement' : `cả ${total} measurement`) : `${n}/${total} measurement`; - toGate.push(`${c.id}: ${label} method-unproven → chưa gate được`); + const label = n === total ? (total === 1 ? 'measurement' : total === 2 ? 'both measurements' : `all ${total} measurements`) : `${n}/${total} measurements`; + toGate.push(`${c.id}: ${label} method-unproven → cannot gate yet`); } // Blocking power is earned against ground truth: a commitment nobody has labeled a run for has no // false-positive rate, and a verdict with no measured error rate cannot gate. const labeled = corpusLabels.some(l => l.journey === c.scope?.journey && (!c.scope?.step || !l.step || l.step === c.scope.step)); - if (c.status === 'ACTIVE' && !n && !labeled) toGate.push(`${c.id}: chưa có nhãn corpus nào cho ${c.scope?.journey || '?'}${c.scope?.step ? '/' + c.scope.step : ''} → FP rate chưa đo, chưa gate được`); - if (c.status === 'RETIREMENT_PROPOSED' && !c.retirement?.decidedBy) toGate.push(`${c.id} RETIREMENT_PROPOSED từ ${day(c.retirement?.at)}, chưa ai quyết`); - if (standingC[c.id].reviewOverdue) toGate.push(`${c.id}: quá reviewAfter ${c.reviewAfter}, chưa ai xem → chỉ được finding`); - if (standingC[c.id].traceDemoted) toGate.push(`${c.id}: trace tới insight đã demote (${standingC[c.id].traceDemoted.join(', ')}) → chờ quyết`); + if (c.status === 'ACTIVE' && !n && !labeled) toGate.push(`${c.id}: no corpus label yet for ${c.scope?.journey || '?'}${c.scope?.step ? '/' + c.scope.step : ''} → FP rate not measured, cannot gate yet`); + if (c.status === 'RETIREMENT_PROPOSED' && !c.retirement?.decidedBy) toGate.push(`${c.id} RETIREMENT_PROPOSED since ${day(c.retirement?.at)}, nobody has decided`); + if (standingC[c.id].reviewOverdue) toGate.push(`${c.id}: past reviewAfter ${c.reviewAfter}, nobody has reviewed it → finding only`); + if (standingC[c.id].traceDemoted) toGate.push(`${c.id}: traces to a demoted insight (${standingC[c.id].traceDemoted.join(', ')}) → awaiting a decision`); } const trust = verified === 0 ? 'observe' : toGate.length ? 'verify' : 'gate'; - if (trust === 'observe') toGate.unshift('chưa có ACTIVE commitment nào với measurement method-validated được đo trên một run completed'); + if (trust === 'observe') toGate.unshift('no ACTIVE commitment yet with a method-validated measurement evaluated on a completed run'); // Reach axis: the most any completed run achieved, and what keeps the next environment shut. const reach = completed.map(r => r.reach?.effective).filter(Boolean).sort((a, b) => rankReach(b) - rankReach(a))[0] || 'observe'; @@ -93,10 +93,10 @@ export function projection({ insights: insightList = [], journeys = [], commitme const fixtureRuns = current.flatMap(r => (r.scenario?.fixtures || []).map(f => ({ ...f, ranAt: r.ranAt }))); for (const p of new Set(fixtureRuns.map(f => f.profile))) { const cleanups = fixtureRuns.filter(f => f.profile === p).map(f => f.cleanup); - if (!cleanups.includes('deleted')) toInject.push(`recoverability của ${p} unverified: cleanup chưa từng chạy sạch trên run fail`); + if (!cleanups.includes('deleted')) toInject.push(`recoverability of ${p} unverified: cleanup has never run clean on a failed run`); } for (const [env, e] of Object.entries(policy.environments || {})) { - if (rankReach(e.reachMax) > rankReach(reach) && !current.some(r => r.environment === env)) toInject.push(`environment ${env} chưa có run nào`); + if (rankReach(e.reachMax) > rankReach(reach) && !current.some(r => r.environment === env)) toInject.push(`environment ${env} has no run yet`); } const L = { observe: 'L1 — Observe', verify: 'L2 — Verify', gate: reach === ORDER[ORDER.length - 1] ? 'L4 — Gate + inject' : 'L3 — Gate' }[trust]; diff --git a/src/core/map.js b/src/core/map.js index cebb355..5de8876 100644 --- a/src/core/map.js +++ b/src/core/map.js @@ -82,7 +82,7 @@ export function mapPage(m) { <title>${esc(m.project.name)} · journey map

Experience

What the person goes through on each journey, from the trace of its last walk in Chrome. Every finding is method-unproven: reported with its source, never a fail and never a pass.

${sections || '

No journey run with steps on disk.

'}
Estimate: keystroke-level model (Card, Moran & Newell) — K ${KLM.K} s, P ${KLM.P} s, BB ${2 * KLM.B} s, H ${KLM.H} s, M ${KLM.M} s, each scroll one P, plus the measured settle time. Response bands after Nielsen (1993): ${LIMITS.instant} ms, ${LIMITS.flow} ms, ${LIMITS.attention} ms.
+`; + fs.mkdirSync(path.dirname(outFile), { recursive: true }); + fs.writeFileSync(outFile, html); + return outFile; +} + +export const pageFile = root => path.join(path.resolve(root), UXCLI, 'experience', 'index.html'); diff --git a/src/journey.js b/src/journey.js index eb2b60e..0980b05 100644 --- a/src/journey.js +++ b/src/journey.js @@ -6,7 +6,7 @@ // .uxcli/index.json recomputed from disk import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import { launch } from './browser.js'; -import { observe, intercept, requestMatches, getPath } from './adapters/chrome/index.js'; +import { observe, intercept, requestMatches, getPath, sha1_8 } from './adapters/chrome/index.js'; import { provision, verifyIdentity, allowedPermissions, cleanup } from './adapters/provision/index.js'; import { parseJourney, prerequisitesOf } from './core/model/journey.js'; import { parseCommitment } from './core/model/commitment.js'; @@ -24,6 +24,7 @@ import { outcomeOf } from './core/run/measure.js'; import { projection } from './core/level/projection.js'; import { UXCLI, newRunDir, artifactsDir, currentRuns, prune } from './adapters/store/runs.js'; import { runHash } from './adapters/store/run-hash.js'; +import { pinnedRuns } from './version.js'; const readJson = f => JSON.parse(fs.readFileSync(f, 'utf8')); const sha256 = s => 'sha256:' + crypto.createHash('sha256').update(s).digest('hex'); @@ -126,7 +127,7 @@ export async function runJourney(file, { env, origin, viewport = '390x844', out const wfId = w => `${journey.id}/${w.id}`; const shell = { ...process.env, UXCLI_TARGET: target }; // Written once. Then older runs of every target beyond KEEP go, except any a commitment anchors to. - const finish = async (run, scenarioFix) => { const sealed = scenarioFix ? await scenarioFix(run) : run; fs.writeFileSync(path.join(dir, 'run.json'), JSON.stringify(sealed, null, 1) + '\n'); prune(root, { pinned: commitments.map(c => c.anchor?.run).filter(Boolean) }); writeProjection(P); return { run: sealed, commitments, root, dir }; }; + const finish = async (run, scenarioFix) => { const sealed = scenarioFix ? await scenarioFix(run) : run; fs.writeFileSync(path.join(dir, 'run.json'), JSON.stringify(sealed, null, 1) + '\n'); prune(root, { pinned: [...commitments.map(c => c.anchor?.run).filter(Boolean), ...pinnedRuns(root)] }); writeProjection(P); return { run: sealed, commitments, root, dir }; }; // Identity: the provisioner's word, checked against the environment. Creating one is a mutation. let identity = null, idValues = null, idProfile = null, idRejected = null; @@ -195,7 +196,7 @@ export async function runJourney(file, { env, origin, viewport = '390x844', out if (fills.length) { try { ref = await observe(page, { action: fills, since: 0, selectors: fills.map(f => f.selector), policy }); } catch (e) { failedFill = e.message; } } const produces = (step.interactions || []).filter(x => x.type === 'api' && x.produces?.length).map(x => ({ request: fill(x.request, params), paths: x.produces })); let obsA = null, failed = failedFill; - if (!failed) try { obsA = await observe(page, { action, selectors: selectorsOf(after, step, [...measurementsAt(step.after), ...measurementsAt(step.id)], params), produces, policy, storageKeys: keysOf(after, step), shotDir: record ? shotDir : null, shotName: shot('after') }); } + if (!failed) try { obsA = await observe(page, { action, selectors: [...new Set([...selectorsOf(after, step, [...measurementsAt(step.after), ...measurementsAt(step.id)], params), ...fills.map(f => f.selector)])], produces, policy, storageKeys: keysOf(after, step), shotDir: record ? shotDir : null, shotName: shot('after') }); } catch (e) { failed = e.message; } for (const { h } of handles) h.release(); const ah = obsA ? holds(after, obsA, { ...ctx, params, before: ref }) : { held: null, strength: after?.strength, signals: {}, why: [`action failed: ${failed}`] }; @@ -208,12 +209,18 @@ export async function runJourney(file, { env, origin, viewport = '390x844', out } if (!record) return true; const interactions = (step.interactions || []).map(x => { - if (x.type === 'ui') { const d = obsB.dom?.[fill(x.target, params)]; return { type: 'ui', target: x.target, ...(d && { inViewportWithoutScroll: d.inViewportWithoutScroll, scrollsNeeded: d.scrollsNeeded, viewport, ...(d.rect && { rect: d.rect }) }), ...(x.consumes && { consumed: x.consumes }) }; } + // A typed value is kept as its length and its hash, never as itself: enough to estimate the typing + // time and to see the same answer asked for twice, and nothing a reader of the packet could use. + if (x.type === 'ui') { const d = obsB.dom?.[fill(x.target, params)]; const v = x.fill !== undefined ? String(fill(x.fill, params)) : null; const seen = v !== null ? obsA?.dom?.[fill(x.target, params)] : null; return { type: 'ui', target: x.target, ...(d && { inViewportWithoutScroll: d.inViewportWithoutScroll, scrollsNeeded: d.scrollsNeeded, viewport, ...(d.rect && { rect: d.rect }) }), ...(v !== null && { typed: { chars: v.length, value: sha1_8(v), ...(seen?.present && seen.value !== null && { keptAfter: seen.value === v }), ...((d?.inputType || seen?.inputType) === 'password' && { secret: true }) } }), ...(x.consumes && { consumed: x.consumes }) }; } if (x.type === 'api') { const n = (obsA?.network || []).find(e => requestMatches(fill(x.request, params), e.method, e.path)); return { type: 'api', request: x.request, ...(n && { status: n.status, ms: n.ms, ...(n.effectClass && { effect: n.effectClass }), ...(n.requestHeaders?.['x-tenant-id'] && { tenantHeader: n.requestHeaders['x-tenant-id'] }), ...(n.blocked && { blocked: true, intercepted: n.intercepted }) }), ...(byInteraction.has(x) && { produced: byInteraction.get(x) }), ...(x.consumes && { consumed: x.consumes }) }; } return { type: x.type, ...(x.to && { to: x.to }), ...(x.expr && { expr: x.expr }), ...(byInteraction.has(x) && { produced: byInteraction.get(x) }), ...(x.consumes && { consumed: x.consumes }) }; }); const shots = [obsB, obsA].flatMap(o => o?.shots || []); + // What the person was told and what they could find their way by, after the action: the visible + // announcements (role=alert, role=status, live regions) by role only, and each navigation landmark's items. + const announced = (obsA?.a11y?.alerts || []).filter(a => a.visible && a.text).map(a => a.role); const r = { id: step.id, workflow: w.id, action: step.action, before: { state: step.before, ...bh }, after: { state: step.after, ...ah, ...(ah.held === false && { what: ah.why.join('; ') }) }, interactions, produced, ...(obsA?.timing && { timing: obsA.timing }), shots, + ...(obsA && { announced, nav: obsA.nav || [] }), ...(handles.length && { intercepted: { request: handles[0].x.request, returned: handles[0].x.intercept.status, blocked: handles.reduce((n, { h }) => n + h.count, 0) } }) }; stepResults.push(r); obsOf.set(r, { before: obsB, after: obsA }); obsList.push({ step: step.id, observation: obsA }); return true; @@ -277,11 +284,11 @@ export async function runJourney(file, { env, origin, viewport = '390x844', out return finish(run, async r => { const retain = r.exit === 2; const expires = new Date(Date.now() + 48 * 3600e3).toISOString(); for (const f of [...fixtures].reverse()) { - if (retain) Object.assign(f.record, { cleanup: 'retained', retainedBecause: 'run có fail — fixture là bằng chứng', expiresAt: expires }); + if (retain) Object.assign(f.record, { cleanup: 'retained', retainedBecause: 'the run has a fail — the fixture is evidence', expiresAt: expires }); else { const c = cleanup(f.prof, f.values, { cwd: root, env: shell }); Object.assign(f.record, { cleanup: c.status, cleanupVerified: c.verified }); } } if (identity && idValues) { - if (retain) Object.assign(identity, { cleanupVerified: false, retained: true, retainedBecause: 'run có fail' }); + if (retain) Object.assign(identity, { cleanupVerified: false, retained: true, retainedBecause: 'the run has a fail' }); else identity.cleanupVerified = cleanup(idProfile.value, idValues, { cwd: root, env: shell }).verified; } return { ...r, scenario: { identity, fixtures: fixtures.map(f => f.record) } }; diff --git a/src/suites.js b/src/suites.js index 21b6811..65cd96f 100644 --- a/src/suites.js +++ b/src/suites.js @@ -35,4 +35,7 @@ export const SUITES = [ { tag: 'map', title: 'drift is what the run said, nothing more', file: 'map-pairs.mjs', miss: 'seen' }, { tag: 'page', title: 'the instrument measures its own pages', file: 'page-pairs.mjs', miss: 'seen' }, { tag: 'lens', title: 'a viewpoint has a name, a review answers all', file: 'lens-pairs.mjs', miss: 'refused' }, + { tag: 'tmpl', title: 'a template asks, and apply only creates', file: 'template-pairs.mjs', miss: 'refused' }, + { tag: 'exp', title: 'what the person goes through, from the trace', file: 'experience-pairs.mjs', miss: 'seen' }, + { tag: 'ver', title: 'a version is kept, and two compare', file: 'version-pairs.mjs', miss: 'refused' }, ]; diff --git a/src/template.js b/src/template.js new file mode 100644 index 0000000..43447b5 --- /dev/null +++ b/src/template.js @@ -0,0 +1,95 @@ +// Project templates on disk: skills/uxcli/templates/.json, and .md beside it — the brief an +// agent reads, installed with the skill. `apply` creates actor files under .uxcli/understanding/actors/ +// and never touches one that exists. +import fs from 'node:fs'; import path from 'node:path'; +import { parseTemplate, actorSeed } from './core/model/template.js'; + +const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..'); +export const TEMPLATE_DIR = path.join(ROOT, 'skills', 'uxcli', 'templates'); +const readJson = f => JSON.parse(fs.readFileSync(f, 'utf8')); + +export function templates() { + const problems = []; + const list = fs.existsSync(TEMPLATE_DIR) ? fs.readdirSync(TEMPLATE_DIR).filter(f => f.endsWith('.json')).sort().map(f => { + const r = parseTemplate(readJson(path.join(TEMPLATE_DIR, f))); + problems.push(...r.problems.map(p => `templates/${f}: ${p}`)); + if (r.value && `${r.value.id}.json` !== f) problems.push(`templates/${f}: id "${r.value.id}" does not match the file name`); + return r.value; + }).filter(Boolean) : []; + return { list, problems }; +} + +export function templateListCard(lib) { + const L = ['uxcli template · where to start for a kind of product', '']; + for (const t of lib.list) { + L.push(` ${t.id.padEnd(10)} ${t.name} · ${t.screens.length} screens · ${t.journeys.length} journeys · ${t.research.length} research questions`); + L.push(` ${''.padEnd(10)} ${t.when}`); + } + for (const p of lib.problems) L.push(` problem ${p}`); + L.push('', ' uxcli template show the brief: screens and their lens, journeys to walk, what to research', + ' uxcli template apply [dir] write each actor\'s questions into .uxcli/understanding/actors/ (creates only)'); + return L.join('\n'); +} + +// The brief, as the agent reads it. The same text is the shipped .md, held equal by the gate. +export function templateMarkdown(t) { + const L = [`# ${t.name} — the \`${t.id}\` template`, '', t.when, '', + 'A template is where research starts, not what it found. Nothing below is a fact about this product\'s users: the screens and journeys are what products of this kind usually have, and every question is yours to answer from a source before it becomes an insight. `uxcli template apply ' + t.id + '` writes the actor questions into `.uxcli/understanding/actors/`; `references/research.md` in this skill is how to answer them.', '', + '## Who uses it', '']; + for (const a of t.actors) { + L.push(`### \`${a.actor}\``, '', a.who, ''); + for (const q of a.questions) L.push(`- ${q}`); + L.push(''); + } + L.push('## Screens, and the lens each is read against', '', '| Screen | Lens | What the person does there |', '|---|---|---|'); + for (const s of t.screens) L.push(`| ${s.name} (\`${s.id}\`) | \`${s.lens}\` | ${s.does} |`); + L.push('', '## Journeys to walk first', ''); + for (const j of t.journeys) { + L.push(`### \`${j.id}\` — ${j.goal}`, '', `Actor \`${j.actor}\`, through: ${j.through.map(s => `\`${s}\``).join(' → ')}.`, '', 'Watch when you walk it:', ''); + for (const w of j.watch) L.push(`- ${w}`); + L.push(''); + } + L.push('## What to research about the domain', ''); + for (const q of t.research) L.push(`- ${q}`); + L.push('', '## Where research starts', ''); + for (const s of t.sources) L.push(`- [${s.name}](${s.url}) — ${s.why}`); + return L.join('\n') + '\n'; +} + +export function templateShowCard(t) { + const L = [`uxcli template show ${t.id} · ${t.name}`, ` ${t.when}`, '', ' who (every line a question until a source answers it)']; + for (const a of t.actors) { L.push(` ${a.actor} — ${a.who}`); for (const q of a.questions) L.push(` ? ${q}`); } + L.push('', ' screens'); + for (const s of t.screens) L.push(` ${s.id.padEnd(18)} lens ${s.lens.padEnd(12)} ${s.does}`); + L.push('', ' journeys to walk first'); + for (const j of t.journeys) { L.push(` ${j.id} — ${j.goal}`, ` ${j.actor}: ${j.through.join(' → ')}`); for (const w of j.watch) L.push(` watch: ${w}`); } + L.push('', ' research'); + for (const q of t.research) L.push(` ? ${q}`); + L.push('', ' sources'); + for (const s of t.sources) L.push(` ${s.name} · ${s.url}`); + L.push('', ` next: uxcli template apply ${t.id}, then answer the questions (references/research.md), then uxcli lens show before drawing`); + return L.join('\n'); +} + +// Creates .uxcli/understanding/actors/.json for each actor the template names and the project +// lacks. An actor file that exists is reported and left alone, whatever it says. +export function applyTemplate(dir, t, { at = new Date().toISOString() } = {}) { + const base = path.join(path.resolve(dir), '.uxcli', 'understanding', 'actors'); + const items = t.actors.map(a => { + const file = path.join(base, `${a.actor}.json`); + if (fs.existsSync(file)) return { file, status: 'kept' }; + fs.mkdirSync(base, { recursive: true }); + fs.writeFileSync(file, JSON.stringify(actorSeed(t, a, at), null, 1) + '\n'); + return { file, status: 'created' }; + }); + return { template: t.id, items }; +} + +export function applyCard(r, t, cwd = process.cwd()) { + const L = [`uxcli template apply ${r.template}`, '']; + for (const it of r.items) L.push(` ${it.status.padEnd(8)} ${path.relative(cwd, it.file)}`); + L.push('', ` ${t.actors.reduce((n, a) => n + a.questions.length, 0)} questions written as unknowns. None is a fact yet.`, + ' next: answer them from sources (references/research.md); each answer is an insight with its evidence.', + ` then: draw with the lens each screen names (uxcli template show ${t.id}).`); + return L.join('\n'); +} diff --git a/src/version.js b/src/version.js new file mode 100644 index 0000000..6a98f37 --- /dev/null +++ b/src/version.js @@ -0,0 +1,67 @@ +// Versions on disk: .uxcli/versions//.json, one file per version, created and never +// edited. A version names a run under .uxcli/runs/; that run is then pinned against pruning. +import fs from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; +import { parseVersion } from './core/model/version.js'; +import { journeyName } from './core/experience.js'; +import { allRuns, UXCLI } from './adapters/store/runs.js'; + +const readJson = f => JSON.parse(fs.readFileSync(f, 'utf8')); +export const versionsDir = root => path.join(root, UXCLI, 'versions'); + +// Every version on disk, oldest first within a journey: { file, value } or { file, problems }. +export function versions(root) { + const base = versionsDir(root); const out = []; + if (!fs.existsSync(base)) return out; + for (const j of fs.readdirSync(base).sort()) { + const dir = path.join(base, j); if (!fs.statSync(dir).isDirectory()) continue; + for (const f of fs.readdirSync(dir).filter(x => x.endsWith('.json')).sort()) { + const file = path.join(dir, f); let r; + try { r = parseVersion(readJson(file)); } catch (e) { r = { value: null, problems: [e.message] }; } + if (r.value && (r.value.journey !== j || `${r.value.name}.json` !== f)) r = { value: null, problems: [`journey and name must match the path ${j}/${f}`] }; + out.push({ file, ...r }); + } + } + return out.sort((a, b) => String(a.value?.at || '').localeCompare(String(b.value?.at || ''))); +} + +// The run directories some version names: pruning keeps them. +export const pinnedRuns = root => versions(root).filter(v => v.value).map(v => v.value.run); + +const gitHead = cwd => { try { return execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim() || null; } catch { return null; } }; + +// Name a walk. `run` is a run directory; without it, the journey's newest walk with steps. +export function saveVersion(root, journey, name, { run = null, note = null, by, at = new Date().toISOString(), proposal = null } = {}) { + const runs = allRuns(root); + const hit = run ? runs.find(x => path.resolve(x.at) === path.resolve(run) || path.basename(x.at) === path.basename(run)) : runs.find(x => journeyName(x.run) === journey && (x.run.steps || []).length); + if (!hit) return { problems: [run ? `no run ${run} under .uxcli/runs/` : `no walk of ${journey} with steps under .uxcli/runs/ — run it first`] }; + if (journeyName(hit.run) !== journey) return { problems: [`${path.basename(hit.at)} is a walk of ${journeyName(hit.run)}, not ${journey}`] }; + const doc = { schema_version: 1, journey, name, run: `runs/${path.basename(hit.at)}`, at, by, ...(note && { note }), ...(gitHead(root) && { build: gitHead(root) }), ...(proposal && { proposal }) }; + const r = parseVersion(doc); if (!r.value) return { problems: r.problems }; + const file = path.join(versionsDir(root), journey, `${name}.json`); + if (fs.existsSync(file)) return { problems: [`${path.relative(root, file)} exists — a version is never rewritten; give the new one another name`] }; + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, JSON.stringify(doc, null, 1) + '\n'); + return { file, value: r.value }; +} + +// A version's walk: its run directory and packet. +export function versionRun(root, journey, name) { + const v = versions(root).find(x => x.value && x.value.journey === journey && x.value.name === name); + if (!v) return null; + const dir = path.join(root, UXCLI, v.value.run); const f = path.join(dir, 'run.json'); + return fs.existsSync(f) ? { dir, run: readJson(f), version: v.value } : { dir, run: null, version: v.value }; +} + +export function versionListCard(root) { + const list = versions(root); const L = ['uxcli version · named walks of each journey, kept and comparable', '']; + if (!list.length) L.push(' none yet — after a walk: uxcli version save --note="what changed"'); + let j = null; + for (const v of list) { + if (!v.value) { L.push(` problem ${path.relative(root, v.file)}: ${v.problems.join('; ')}`); continue; } + if (v.value.journey !== j) { j = v.value.journey; L.push(` ${j}`); } + const missing = !fs.existsSync(path.join(root, UXCLI, v.value.run, 'run.json')); + L.push(` ${v.value.name.padEnd(12)} ${v.value.at.slice(0, 16)} · ${v.value.by.type} ${v.value.by.ref}${v.value.build ? ` · ${v.value.build}` : ''}${missing ? ' · its run is gone' : ''}${v.value.note ? ` — ${v.value.note}` : ''}`); + } + L.push('', ' uxcli experience . --journey= --from= --to= --page two versions, step by step'); + return L.join('\n'); +} diff --git a/test/context-pairs.mjs b/test/context-pairs.mjs index b3059af..352cae4 100644 --- a/test/context-pairs.mjs +++ b/test/context-pairs.mjs @@ -2,7 +2,7 @@ import { readContext, standingOf, citable, citationOf, STANDINGS, FIELDS } from export const OPERATOR = 'a field quoting words that are really in the document, against the same field once the document no longer says them — which is what happened to this project\'s own declaration and which the old rule, asking only whether `source` was a non-empty string, reported as known; and the three honest ways a citation says less than that, none of which may be read as a mismatch: a source that is only a name, a document that could not be read, and a field nobody wrote.'; -const DOC = 'Trước khi nó được phép nói "xong", sản phẩm đang chạy\nphải được đối chiếu với những gì đã được hứa.'; +const DOC = 'Before it may say "done", the running product\nmust be checked against what was promised.'; const at = (text, tracked = true) => ({ 'spec.md': { found: true, text, tracked } }); const field = (source, value = 'x') => ({ value, source }); @@ -11,14 +11,14 @@ export function pair() { let checks = 0; const is = (got, want, what) => { checks++; if (got !== want) problems.push(`${what}: ${JSON.stringify(got)}, wanted ${JSON.stringify(want)}`); }; - const cited = field({ doc: 'spec.md', quote: 'phải được đối chiếu với những gì đã được hứa' }); + const cited = field({ doc: 'spec.md', quote: 'must be checked against what was promised' }); // The pair. Same declaration, same quote; only the document moves. is(standingOf(cited, at(DOC)).standing, 'quoted', 'a quote that is in the document'); - is(standingOf(cited, at('Tài liệu này đã được viết lại.')).standing, 'drifted', 'the document no longer says it'); + is(standingOf(cited, at('This document has been rewritten.')).standing, 'drifted', 'the document no longer says it'); // A quote is words, not bytes: a sentence re-wrapped when the file was edited is the same sentence. - is(standingOf(field({ doc: 'spec.md', quote: 'sản phẩm đang chạy phải được đối chiếu' }), at(DOC)).standing, + is(standingOf(field({ doc: 'spec.md', quote: 'the running product must be checked' }), at(DOC)).standing, 'quoted', 'a quote that spans a line break in the document'); // The three ways of saying less, each its own word. None of them is `drifted`: a citation that was diff --git a/test/experience-pairs.mjs b/test/experience-pairs.mjs new file mode 100644 index 0000000..2b0fa1c --- /dev/null +++ b/test/experience-pairs.mjs @@ -0,0 +1,114 @@ +// Experience metrics read the trace of a walk. Each is a pair: a clean walk where it must stay silent, +// and the same walk with one thing a person would feel planted — a wait, a control below the fold, an +// answer asked for twice, a step that does not arrive, an extra step — where it must speak. The example +// project's last walk must surface the defect its own commitment C-001 is about. +import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath, pathToFileURL } from 'node:url'; +import { experience, compareExperience, KLM } from '../src/core/experience.js'; +import { experiences, experiencePage } from '../src/experience.js'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const clone = o => JSON.parse(JSON.stringify(o)); + +export const OPERATOR = 'a clean three-step walk with no finding, against the same walk made to settle in 2.4 s, to need its button two scrolls down, to ask for an email typed one step earlier, to miss the state a step must reach, and to take one more step; an error answered with the typed answer kept and a message announced, against one that clears the answer and one that announces nothing, and a packet that never recorded announcements, which is not silence; a navigation that keeps its order or only gains an item, against one whose items swap; the estimate grows by the operators of the planted step and nothing else; a comparison of the two walks names what moved and which findings came and went; a fixture step is not a step the person takes; and the example project\'s last walk, whose call button C-001 says is reachable without scrolling, must report it two scrolls away'; + +const ui = (target, extra = {}) => ({ type: 'ui', target, inViewportWithoutScroll: true, scrollsNeeded: 0, viewport: '390x844', rect: { x: 20, y: 300, w: 350, h: 48, viewport: { w: 390, h: 844 } }, ...extra }); +const typed = (chars, value) => ({ typed: { chars, value } }); +const CLEAN = { + journey: { ref: 'journeys/checkout.json' }, ranAt: '2026-10-03T00:00:00Z', + steps: [ + { id: 'f0', kind: 'fixture', ms: 400 }, + { id: 's1', workflow: 'buy', action: 'type the email and continue', after: { state: 'details', held: true }, interactions: [ui('input[name=email]', typed(17, 'sha1_8:aaaa1111')), ui('[data-uxcli=continue]')], timing: { toStable: 80 }, shots: ['buy-s1-before.png', 'buy-s1-after.png'] }, + { id: 's2', workflow: 'buy', action: 'type the address and continue', after: { state: 'review', held: true }, interactions: [ui('input[name=address]', typed(24, 'sha1_8:bbbb2222')), ui('[data-uxcli=continue]')], timing: { toStable: 300 }, shots: ['buy-s2-before.png', 'buy-s2-after.png'] }, + { id: 's3', workflow: 'buy', action: 'place the order', after: { state: 'done', held: true }, interactions: [ui('[data-uxcli=place-order]')], timing: { toStable: 600 }, shots: ['buy-s3-before.png', 'buy-s3-after.png'] }, + ], +}; + +export async function pair() { + const problems = []; let checks = 0; + const must = (what, cond) => { checks++; if (!cond) problems.push(what); }; + const metrics = r => r.findings.map(f => f.metric); + + // must-pass: the clean walk + const clean = experience(CLEAN); + must(`the clean walk reported ${JSON.stringify(clean.findings)}`, clean.findings.length === 0); + must('a fixture step was counted as a step the person takes', clean.totals.steps === 3); + must('the clean totals are wrong', clean.totals.clicks === 3 && clean.totals.fields === 2 && clean.totals.chars === 41 && clean.totals.scrolls === 0 && clean.totals.reached === 3); + const click = KLM.M + KLM.P + 2 * KLM.B; const field = n => click + 2 * KLM.H + n * KLM.K; + const want = Math.round((field(17) + click + 0.08 + field(24) + click + 0.3 + click + 0.6) * 10) / 10; + must(`the estimate ${clean.totals.klmSeconds} s is not the operators' sum ${want} s`, Math.abs(clean.totals.klmSeconds - want) < 0.21); + must('a response band is wrong', clean.steps.map(s => s.response).join(' ') === 'instant flow flow'); + + // must-fail: each thing a person would feel, planted once + const slow = clone(CLEAN); slow.steps[3].timing.toStable = 2400; + must('a 2.4 s wait was not reported', metrics(experience(slow)).includes('response')); + const lost = clone(CLEAN); lost.steps[3].timing.toStable = 12000; + must('a 12 s wait was not reported as lost attention', experience(lost).findings.some(f => f.metric === 'response' && /10 s/.test(f.what))); + const far = clone(CLEAN); far.steps[3].interactions[0] = ui('[data-uxcli=place-order]', { inViewportWithoutScroll: false, scrollsNeeded: 2 }); + const farR = experience(far); + must('a button two scrolls down was not reported', farR.findings.some(f => f.metric === 'reach' && f.step === 's3' && /2 scrolls/.test(f.what) && f.rect)); + must('scrolls did not add to the estimate', farR.totals.klmSeconds > clean.totals.klmSeconds && farR.totals.scrolls === 2); + const twice = clone(CLEAN); twice.steps[2].interactions[0] = ui('input[name=confirm-email]', typed(17, 'sha1_8:aaaa1111')); + must('an answer typed twice was not reported', experience(twice).findings.some(f => f.metric === 'asked-twice' && f.step === 's2' && /s1/.test(f.what))); + const other = clone(twice); other.steps[2].workflow = 'another'; + must('the same value in another workflow was reported as asked twice', !metrics(experience(other)).includes('asked-twice')); + const miss = clone(CLEAN); miss.steps[3].after = { state: 'done', held: false, what: '[data-uxcli=order-number] visible: false' }; + must('a step that did not arrive was not reported', experience(miss).findings.some(f => f.metric === 'not-reached' && /order-number/.test(f.what))); + const longer = clone(CLEAN); longer.steps.splice(3, 0, { id: 's2b', workflow: 'buy', action: 'confirm the address', after: { state: 'confirmed', held: true }, interactions: [ui('[data-uxcli=confirm]')], timing: { toStable: 100 }, shots: [] }); + const longR = experience(longer); + must('an extra step did not add one step and one click', longR.totals.steps === 4 && longR.totals.clicks === 4); + must('an extra step did not add its operators to the estimate', Math.abs(longR.totals.klmSeconds - clean.totals.klmSeconds - (click + 0.1)) < 0.21); + + // recovery: an error answer keeps what was typed and says so; planted, it does neither + const errStep = (kept, announced) => ({ id: 'e1', workflow: 'buy', action: 'continue while the server fails', after: { state: 'details-error', held: true }, + interactions: [ui('input[name=email]', { typed: { chars: 17, value: 'sha1_8:cccc3333', keptAfter: kept } }), ui('[data-uxcli=continue]'), { type: 'api', request: 'POST /api/details', status: 500, ms: 40 }], + timing: { toStable: 90 }, shots: [], announced, nav: [] }); + const okErr = clone(CLEAN); okErr.steps.push({ ...errStep(true, ['alert']), workflow: 'server-error' }); + must(`an error that kept the answer and announced itself was reported: ${JSON.stringify(experience(okErr).findings)}`, !metrics(experience(okErr)).includes('recovery')); + const cleared = clone(CLEAN); cleared.steps.push({ ...errStep(false, ['alert']), workflow: 'server-error' }); + must('an error that cleared the typed answer was not reported', experience(cleared).findings.some(f => f.metric === 'recovery' && /no longer holds/.test(f.what))); + const pw = clone(CLEAN); const p = errStep(true, ['alert']); p.interactions.unshift(ui('input[name=password]', { typed: { chars: 12, value: 'sha1_8:dddd4444', keptAfter: false, secret: true } })); pw.steps.push({ ...p, workflow: 'server-error' }); + must('a password cleared after the error was reported as a lost answer', !metrics(experience(pw)).includes('recovery')); + const silent = clone(CLEAN); silent.steps.push({ ...errStep(true, []), workflow: 'server-error' }); + must('an error nothing announced was not reported', experience(silent).findings.some(f => f.metric === 'recovery' && /announced/.test(f.what))); + const unknownAnn = clone(CLEAN); const u = errStep(true, undefined); delete u.announced; unknownAnn.steps.push({ ...u, workflow: 'server-error' }); + must('a packet that never recorded announcements was read as silence', !metrics(experience(unknownAnn)).includes('recovery')); + // consistency: one navigation, the same order on every screen; planted, two items swap + const NAV = ['Home', 'Shop', 'Cart', 'Account']; + const withNav = order => { const r = clone(CLEAN); r.steps[1].nav = [{ label: 'Main', items: NAV }]; r.steps[2].nav = [{ label: 'Main', items: NAV }]; r.steps[3].nav = [{ label: 'Main', items: order }]; return r; }; + must('the same navigation on every screen was reported', !metrics(experience(withNav(NAV))).includes('consistency')); + must('a navigation that only gained an item was reported', !metrics(experience(withNav([...NAV, 'Help']))).includes('consistency')); + must('two navigation items that swapped places were not reported', experience(withNav(['Home', 'Cart', 'Shop', 'Account'])).findings.some(f => f.metric === 'consistency' && f.step === 's3' && /Main/.test(f.what))); + + // alternatives are not a longer journey: a second workflow is totalled on its own + const alt = clone(CLEAN); alt.steps.push({ id: 'r1', workflow: 'card-declined', action: 'place the order with a declined card', after: { state: 'declined', held: true }, interactions: [ui('[data-uxcli=place-order]')], timing: { toStable: 200 }, shots: [] }); + const altR = experience(alt); + must('an alternative workflow was added to the happy path\'s totals', altR.totals.steps === 3 && altR.workflows.length === 2 && altR.workflows[1].totals.steps === 1); + // versions: the comparison names what moved + const cmp = compareExperience(clean, farR); + must('the comparison did not name the scrolls that appeared', cmp.totals.scrolls.delta === 2 && cmp.introduced.length === 1 && cmp.fixed.length === 0); + const back = compareExperience(farR, clean); + must('the comparison did not name the finding that went away', back.fixed.length === 1 && back.introduced.length === 0); + + // the example project: its last walk carries the defect C-001 is signed against + const ex = experiences(path.join(ROOT, 'examples', 'crm')); + const lead = ex.find(x => x.report.journey === 'handle-inbound-lead'); + must('the example\'s handle-inbound-lead walk was not found', !!lead); + must('the example\'s call button was not reported two scrolls away at 390x844', !!lead && lead.report.findings.some(f => f.metric === 'reach' && /call-action/.test(f.what) && /2 scrolls/.test(f.what) && /390x844/.test(f.what))); + + // the page: written where asked, one card per step, the pinned finding drawn as a box + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'uxcli-exp-')); + fs.mkdirSync(path.join(tmp, 'artifacts')); fs.writeFileSync(path.join(tmp, 'artifacts', 'buy-s3-before.png'), Buffer.from('89504e470d0a1a0a', 'hex')); + const out = experiencePage([{ dir: tmp, report: farR }], path.join(tmp, 'experience', 'index.html')); + const html = fs.readFileSync(out, 'utf8'); + must('the page does not carry one card per step', (html.match(/
({ ...c, measurements: c.measurements.map(m => ({ ...m, method: 'method-validated' })) })); const validated = projection(validatedIn); - must('validating C-002 did not turn its method sentence into a corpus sentence', !validated.level.toNext['trust → gate'].some(s => s.startsWith('C-002') && s.includes('method-unproven')) && validated.level.toNext['trust → gate'].some(s => s.startsWith('C-002') && s.includes('nhãn corpus')) && validated.level.trust === 'verify'); + must('validating C-002 did not turn its method sentence into a corpus sentence', !validated.level.toNext['trust → gate'].some(s => s.startsWith('C-002') && s.includes('method-unproven')) && validated.level.toNext['trust → gate'].some(s => s.startsWith('C-002') && s.includes('no corpus label')) && validated.level.trust === 'verify'); // Gate is earned against ground truth: only a corpus label on this commitment's step clears the last sentence. const labeled = projection({ ...validatedIn, corpusLabels: [...input.corpusLabels, { id: 'L-test', journey: 'handle-inbound-lead', step: 's1', groundTruth: 'pass', blind: true, labeledBy: [{ who: 'test' }] }] }); must('a corpus label on C-002\'s step did not clear its sentence', !labeled.level.toNext['trust → gate'].some(s => s.startsWith('C-002'))); const unlabeled = projection({ ...input, corpusLabels: [] }); - must('with no corpus label at all, C-001 (validated, measured) still had no corpus sentence', unlabeled.level.toNext['trust → gate'].some(s => s.startsWith('C-001') && s.includes('nhãn corpus'))); + must('with no corpus label at all, C-001 (validated, measured) still had no corpus sentence', unlabeled.level.toNext['trust → gate'].some(s => s.startsWith('C-001') && s.includes('no corpus label'))); const cleaned = projection({ ...input, runs: input.runs.map(r => r.run.id !== 'j-handle-inbound-lead' ? r : { ...r, run: { ...r.run, scenario: { ...r.run.scenario, fixtures: r.run.scenario.fixtures.map(f => ({ ...f, cleanup: 'deleted' })) } } }) }); must('a verified cleanup did not remove the recoverability sentence', !cleaned.level.toNext['reach → inject'].some(s => s.startsWith('recoverability'))); diff --git a/test/measure-pairs.mjs b/test/measure-pairs.mjs index 4261b05..4232439 100644 --- a/test/measure-pairs.mjs +++ b/test/measure-pairs.mjs @@ -23,7 +23,7 @@ export function pair() { must('a state met at two steps was not read at the scope step', o.where === 's2'); must('the before side was not preferred at the scope step', o.shot === 's2-before.png'); must('a button two scrolls down was not not-held', o.outcome === 'not-held'); - must('how far the button sits was not in the outcome', /2 lần cuộn ở 390x844/.test(o.what || '')); + must('how far the button sits was not in the outcome', /2 scrolls at 390x844/.test(o.what || '')); const held = outcomeOf({ ...c1, scope: { step: 's1' } }, c1.measurements[0], 0, steps, {}); must('a button in view at s1 was not held', held.outcome === 'held' && held.where === 's1'); must('prose predicate was not unmeasurable', outcomeOf(c1, { target: 'agent.lead_detail', predicate: 'in viewport' }, 0, steps).outcome === 'unmeasurable'); diff --git a/test/model-pairs.mjs b/test/model-pairs.mjs index 5216214..5fd4c32 100644 --- a/test/model-pairs.mjs +++ b/test/model-pairs.mjs @@ -83,7 +83,7 @@ export function pair() { must('discrimination check missed a mustNotMatch entry that is not even a sentence', discriminationProblems(j).filter(p => p.includes('is not a state name')).length === 2); j = clone(auth); j.states['anon.login_page'].mustNotMatch = ['anon.login_page']; must('discrimination check let a state exclude itself', discriminationProblems(j).some(p => p.includes('cannot be what it must not match'))); - j = clone(auth); j.states['anon.login_page'].mustNotMatch = ['khi đang tải']; + j = clone(auth); j.states['anon.login_page'].mustNotMatch = ['while loading']; must('discrimination check refused a prose entry, which the runtime leaves unchecked', discriminationProblems(j).length === 0); must('discrimination check objected to the example', discriminationProblems(auth).length === 0 && discriminationProblems(lead).length === 0); const pre = prerequisitesOf(lead); @@ -97,7 +97,7 @@ export function pair() { // Commitment — one owner, and a quote the document still says. let c = clone(c001); c.owner = [c.owner, { type: 'role', ref: 'engineering-lead' }]; must('commitment parser accepted two owners', says(parseCommitment(c, { docs }), 'owner: one signer, not a list')); - c = clone(c001); c.source.quote = 'hành động nằm rất xa dữ liệu đang xem'; + c = clone(c001); c.source.quote = 'the action sits far from the data being viewed'; must('commitment parser accepted a quote the document does not say', says(parseCommitment(c, { docs }), 'source.quote: understanding/actors/real-estate-agent.json no longer says')); c = clone(c001); delete c.source.quote; must('commitment parser accepted a document named without its words', says(parseCommitment(c, { docs }), 'source.quote: required')); diff --git a/test/predicate-pairs.mjs b/test/predicate-pairs.mjs index 6f7f174..0bc048b 100644 --- a/test/predicate-pairs.mjs +++ b/test/predicate-pairs.mjs @@ -25,7 +25,7 @@ const STATES = { 'agent.workspace_ready': { signals: [{ observer: 'url', path: '/workspace/{workspaceId}' }, { observer: 'network', request: 'GET /api/me', status: 200 }, { observer: 'dom', selector: '[data-uxcli=lead-list]', visible: true }], - strength: 'strong', mustMatch: ['/workspace/*'], mustNotMatch: ['/login', '/onboarding', '/workspace/* khi GET /api/me → 401'], + strength: 'strong', mustMatch: ['/workspace/*'], mustNotMatch: ['/login', '/onboarding', '/workspace/* when GET /api/me → 401'], }, 'anon.login_failed_retryable': { signals: [{ observer: 'a11y', role: 'alert', visible: true }, { observer: 'dom', selector: '[data-uxcli=login-submit]', enabled: true }, @@ -134,7 +134,7 @@ export function pair() { const ALERT = { signals: [{ observer: 'a11y', role: 'alert', visible: true }] }; is(holds(ALERT, { a11y: { alerts: [{ role: 'alert', text: '', visible: true }] } }).held, false, 'a mounted, empty alert region does not satisfy role=alert visible'); is(holds(ALERT, { a11y: { alerts: [{ role: 'alert', text: ' ', visible: true }] } }).held, false, 'whitespace is not an alert either'); - is(holds(ALERT, { a11y: { alerts: [{ role: 'alert', text: '', visible: true }, { role: 'alert', text: 'Mật khẩu không đúng', visible: true }] } }).held, true, 'the same page with a region that says something holds'); + is(holds(ALERT, { a11y: { alerts: [{ role: 'alert', text: '', visible: true }, { role: 'alert', text: 'Incorrect password', visible: true }] } }).held, true, 'the same page with a region that says something holds'); // Caps lower fail to finding, and touch nothing else. is(capFor({ method: 'method-validated', strength: 'strong' }), [], 'a validated method on a strong state is uncapped'); @@ -148,7 +148,7 @@ export function pair() { // The example run: runs/j-authenticate server-error/r1. After the intercepted 500 the alert shows, the // button is enabled, and the email field is empty — the state is not held and the email signal says why. const before = { url: { path: '/login' }, dom: { 'input[name=email]': { present: true, value: sha8('agent@mail.sink.local') } } }; - const after = emailValue => ({ url: { path: '/login' }, a11y: { alerts: [{ role: 'alert', text: 'Lỗi máy chủ, thử lại', visible: true }] }, + const after = emailValue => ({ url: { path: '/login' }, a11y: { alerts: [{ role: 'alert', text: 'Server error, try again', visible: true }] }, dom: { '[data-uxcli=login-submit]': { present: true, visible: true, enabled: true }, 'input[name=email]': { present: true, value: emailValue } }, network: [] }); const r1 = holds(STATES['anon.login_failed_retryable'], after(''), { before, hash }); is([r1.held, r1.strength], [false, 'medium'], 'server-error/r1: the state is not held'); diff --git a/test/run-pairs.mjs b/test/run-pairs.mjs index 35cb387..2383690 100644 --- a/test/run-pairs.mjs +++ b/test/run-pairs.mjs @@ -43,7 +43,7 @@ export function pair() { const okPlan = plan(lead, policy, 'staging', exLead.scenario, { prerequisites: ['agent.workspace_ready'] }); must('staging plan with identity did not come back ok', okPlan.status === 'ok' && eq(okPlan.unverified, ['agent.workspace_ready'])); const noIdPlan = plan(lead, policy, 'staging', { identity: null, fixtures: [] }, { prerequisites: ['agent.workspace_ready'] }); - must('staging without identity did not block on the prerequisite', noIdPlan.status === 'blocked' && noIdPlan.reason === 'prerequisite_not_satisfied' && noIdPlan.which.some(w => w.startsWith('f0 lead-new-unassigned: fixture cần mutate, effective reach là interact'))); + must('staging without identity did not block on the prerequisite', noIdPlan.status === 'blocked' && noIdPlan.reason === 'prerequisite_not_satisfied' && noIdPlan.which.some(w => w.startsWith('f0 lead-new-unassigned: fixture needs mutate, effective reach is interact'))); const rejected = plan(lead, policy, 'staging', { identity: { ...exLead.scenario.identity, verified: { ...exLead.scenario.identity.verified, tenant: false } }, fixtures: [] }, { prerequisites: [] }); must('an identity that failed tenant verification was not rejected', rejected.status === 'blocked' && rejected.reason === 'identity_rejected'); must('an intercept workflow under observe was not reach_insufficient', plan(auth, policy, 'production', { identity: null }, { workflow: 'server-error' }).reason === 'reach_insufficient'); @@ -98,16 +98,16 @@ export function pair() { // Constraints: false is a breach, null is recorded and nothing more. const cs = exLead.reach.constraints; - const broken = packet({ ...base, constraints: cs.map(c => c.id !== 'no-real-sms' ? c : { ...c, held: false, observed: '1 egress tới sms-provider' }) }); + const broken = packet({ ...base, constraints: cs.map(c => c.id !== 'no-real-sms' ? c : { ...c, held: false, observed: '1 egress to sms-provider' }) }); must('a constraint measured false did not abort with a breach', broken.status === 'aborted' && broken.breaches.some(b => b.constraint === 'no-real-sms' && b.rule === 'constraint-not-held')); const unknown = packet({ ...base, constraints: cs.map(c => c.id !== 'no-real-sms' ? c : { ...c, held: null }) }); must('a constraint that could not be observed was treated as a breach', unknown.status === 'completed' && unknown.breaches.length === 0 && unknown.reach.constraints.some(c => c.held === null)); // Runtime blocks, from the observer's own results. - must('a prerequisite measured false did not block', eq(blockedAt({ prerequisites: [{ step: 's1', state: 'agent.workspace_ready', held: false, why: ['GET /api/me → 401'] }] }), { reason: 'prerequisite_not_satisfied', which: ['s1.before agent.workspace_ready: không giữ — GET /api/me → 401'], fix: FIX.prerequisite_not_satisfied })); + must('a prerequisite measured false did not block', eq(blockedAt({ prerequisites: [{ step: 's1', state: 'agent.workspace_ready', held: false, why: ['GET /api/me → 401'] }] }), { reason: 'prerequisite_not_satisfied', which: ['s1.before agent.workspace_ready: not held — GET /api/me → 401'], fix: FIX.prerequisite_not_satisfied })); must('a prerequisite measured null blocked', blockedAt({ prerequisites: [{ step: 's1', state: 'agent.workspace_ready', held: null }] }) === null); - must('a rejected identity did not block as identity_rejected', eq(blockedAt({ identity: { rejected: [{ field: 'tenantId', why: 'không thuộc syntheticTenants' }] } }), { reason: 'identity_rejected', which: ['tenantId: không thuộc syntheticTenants'], fix: FIX.identity_rejected })); - must('a fixture derive() could not value did not block', eq(blockedAt({ fixtures: [{ step: 'f0', profile: 'lead-new-unassigned', problems: ['leads.responseSlaMinutes không có giá trị'] }] }), { reason: 'prerequisite_not_satisfied', which: ['f0 lead-new-unassigned: leads.responseSlaMinutes không có giá trị'], fix: FIX.prerequisite_not_satisfied })); + must('a rejected identity did not block as identity_rejected', eq(blockedAt({ identity: { rejected: [{ field: 'tenantId', why: 'not in syntheticTenants' }] } }), { reason: 'identity_rejected', which: ['tenantId: not in syntheticTenants'], fix: FIX.identity_rejected })); + must('a fixture derive() could not value did not block', eq(blockedAt({ fixtures: [{ step: 'f0', profile: 'lead-new-unassigned', problems: ['leads.responseSlaMinutes has no value'] }] }), { reason: 'prerequisite_not_satisfied', which: ['f0 lead-new-unassigned: leads.responseSlaMinutes has no value'], fix: FIX.prerequisite_not_satisfied })); // Effects from the observer: the class, never a blocked request. const net = [{ method: 'POST', path: '/api/calls', effectClass: 'database_write' }, { method: 'POST', path: '/api/login', effectClass: 'account_mutation', blocked: true, intercepted: { status: 500 } }, { method: 'GET', path: '/api/me' }]; diff --git a/test/template-pairs.mjs b/test/template-pairs.mjs new file mode 100644 index 0000000..f7fb20d --- /dev/null +++ b/test/template-pairs.mjs @@ -0,0 +1,67 @@ +// Project templates hold two promises, and each is a pair. A template states no fact about anyone's +// users — every actor line is a question, every journey names screens and actors the template has — +// and `template apply` only ever creates: an actor file that exists is left as it was, byte for byte. +import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath, pathToFileURL } from 'node:url'; +import { parseTemplate } from '../src/core/model/template.js'; +import { templates, templateMarkdown, applyTemplate, TEMPLATE_DIR } from '../src/template.js'; +import { validate } from './lib/json-schema.mjs'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const schema = name => JSON.parse(fs.readFileSync(path.join(ROOT, 'schemas', `${name}.schema.json`), 'utf8')); +const clone = o => JSON.parse(JSON.stringify(o)); + +export const OPERATOR = 'every shipped template parses, satisfies its schema and matches its brief; a template whose actor line states a fact instead of asking, whose journey names a screen or an actor it does not have, or whose screen names a lens that does not exist is refused; apply writes one actor file per actor whose unknowns are the questions and which satisfies the actor schema, and on a second run, and over an actor file somebody wrote, changes nothing'; + +export async function pair() { + const problems = []; let checks = 0; + const must = (what, cond) => { checks++; if (!cond) problems.push(what); }; + const lib = templates(); + must(`shipped templates have problems: ${lib.problems.join('; ')}`, !lib.problems.length); + must('fewer than three templates ship', lib.list.length >= 3); + for (const t of lib.list) { + const raw = JSON.parse(fs.readFileSync(path.join(TEMPLATE_DIR, `${t.id}.json`), 'utf8')); + const bad = validate(schema('template'), raw); must(`templates/${t.id}.json fails template.schema.json: ${bad.slice(0, 3).join('; ')}`, !bad.length); + const md = path.join(TEMPLATE_DIR, `${t.id}.md`); + must(`skills/uxcli/templates/${t.id}.md is missing or differs from ${t.id}.json — rebuild it with templateMarkdown()`, fs.existsSync(md) && fs.readFileSync(md, 'utf8') === templateMarkdown(t)); + } + + // must-fail: each way a template could start stating facts or pointing at nothing + const base = JSON.parse(fs.readFileSync(path.join(TEMPLATE_DIR, `${lib.list[0].id}.json`), 'utf8')); + const refused = (what, mutate, word) => { const d = clone(base); mutate(d); const r = parseTemplate(d); must(`${what} was accepted`, !r.value && r.problems.some(p => p.includes(word))); }; + refused('an actor line that states a fact', d => { d.actors[0].questions[0] = 'They work on a phone.'; }, 'is not a question'); + refused('a journey through a screen the template lacks', d => { d.journeys[0].through.push('nowhere'); }, 'not one of this template\'s screens'); + refused('a journey for an actor the template lacks', d => { d.journeys[0].actor = 'nobody'; }, 'is not one of this template\'s actors'); + refused('a screen read against a lens that does not exist', d => { d.screens[0].lens = 'taste'; }, 'lens: one of'); + refused('a research line that is not a question', d => { d.research[0] = 'Users want speed.'; }, 'is not a question'); + refused('an unknown key', d => { d.score = 10; }, 'unknown key'); + + // apply: creates, and only creates + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'uxcli-template-')); + const t = lib.list[0]; + const r1 = applyTemplate(tmp, t, { at: '2026-10-03T00:00:00Z' }); + must('apply did not create one actor file per actor', r1.items.length === t.actors.length && r1.items.every(i => i.status === 'created')); + for (const a of t.actors) { + const f = path.join(tmp, '.uxcli', 'understanding', 'actors', `${a.actor}.json`); + const doc = fs.existsSync(f) ? JSON.parse(fs.readFileSync(f, 'utf8')) : null; + must(`${a.actor}: the written unknowns are not the template's questions`, doc && JSON.stringify(doc.unknowns) === JSON.stringify(a.questions)); + must(`${a.actor}: the written actor states something other than unknowns`, doc && !['jobs', 'contexts', 'expectations', 'pains', 'behaviors', 'habits'].some(k => k in doc)); + const bad = doc ? validate(schema('actor'), doc) : ['missing']; must(`${a.actor}: the written actor fails actor.schema.json: ${bad.join('; ')}`, !bad.length); + } + const before = fs.readFileSync(path.join(tmp, '.uxcli', 'understanding', 'actors', `${t.actors[0].actor}.json`), 'utf8'); + const r2 = applyTemplate(tmp, t, { at: '2026-10-04T00:00:00Z' }); + must('a second apply did not keep every file', r2.items.every(i => i.status === 'kept')); + must('a second apply changed a file it had written', fs.readFileSync(path.join(tmp, '.uxcli', 'understanding', 'actors', `${t.actors[0].actor}.json`), 'utf8') === before); + // must-fail planted: an actor somebody already wrote is never overwritten + const own = path.join(tmp, '.uxcli', 'understanding', 'actors', `${t.actors[1]?.actor || t.actors[0].actor}.json`); + fs.writeFileSync(own, '{"schema_version":1,"actor":"mine","unknowns":["written by a person?"]}\n'); + applyTemplate(tmp, t); + must('apply overwrote an actor file somebody wrote', fs.readFileSync(own, 'utf8') === '{"schema_version":1,"actor":"mine","unknowns":["written by a person?"]}\n'); + fs.rmSync(tmp, { recursive: true, force: true }); + return { ok: !problems.length, checks, problems }; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const r = await pair(); + console.log(r.ok ? `PASS template · ${r.checks} checks` : 'FAIL ' + r.problems.join('\n ')); + process.exit(r.ok ? 0 : 1); +} diff --git a/test/version-pairs.mjs b/test/version-pairs.mjs new file mode 100644 index 0000000..124c459 --- /dev/null +++ b/test/version-pairs.mjs @@ -0,0 +1,89 @@ +// A version names one walk of a journey so it is kept and can be compared. Each promise is a pair: a +// version is created and never rewritten; it names a walk of its own journey or nothing; the walk it +// names survives pruning while an unnamed one of the same age does not; two versions compare step by +// step, with both pictures side by side on the page. +import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath, pathToFileURL } from 'node:url'; +import { parseVersion } from '../src/core/model/version.js'; +import { saveVersion, versions, pinnedRuns } from '../src/version.js'; +import { versionComparison, experiencePage } from '../src/experience.js'; +import { prune } from '../src/adapters/store/runs.js'; +import { validate } from './lib/json-schema.mjs'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const schema = JSON.parse(fs.readFileSync(path.join(ROOT, 'schemas', 'version.schema.json'), 'utf8')); +const EXAMPLE = path.join(ROOT, 'examples', 'crm'); + +export const OPERATOR = 'the example\'s versions satisfy the schema and the parser; a version without a walk, by nobody, or with an unknown key is refused; save writes one file for the newest walk of the journey and refuses to rewrite it, to name a walk of another journey, and to name a journey never walked; of eight walks of one journey pruned to seven the oldest goes unless a version names it; two versions compare with the steps and the estimate that moved, and the page shows both pictures of a step side by side'; + +export async function pair() { + const problems = []; let checks = 0; + const must = (what, cond) => { checks++; if (!cond) problems.push(what); }; + + // the shipped example + const ex = versions(EXAMPLE); + must('the example carries no version', ex.length >= 2); + for (const v of ex) { + must(`${path.relative(ROOT, v.file)}: ${(v.problems || []).join('; ')}`, !!v.value); + const bad = validate(schema, JSON.parse(fs.readFileSync(v.file, 'utf8'))); must(`${path.relative(ROOT, v.file)} fails version.schema.json: ${bad.join('; ')}`, !bad.length); + } + const base = JSON.parse(fs.readFileSync(ex[0].file, 'utf8')); + for (const [what, mutate, word] of [['a version with no walk', d => { delete d.run; }, 'run:'], ['a version by nobody', d => { delete d.by; }, 'by:'], ['an unknown key', d => { d.score = 1; }, 'unknown key']]) { + const d = JSON.parse(JSON.stringify(base)); mutate(d); const r = parseVersion(d); + must(`${what} was accepted`, !r.value && r.problems.some(p => p.includes(word))); + must(`${what} was accepted by the schema`, validate(schema, d).length > 0); + } + + // save: creates, names the newest walk of the journey, refuses the rest + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'uxcli-version-')); + fs.cpSync(path.join(EXAMPLE, '.uxcli', 'runs'), path.join(tmp, '.uxcli', 'runs'), { recursive: true }); + const by = { type: 'person', ref: 'test' }; + const r1 = saveVersion(tmp, 'handle-inbound-lead', 'before', { by, note: 'the lead as it is' }); + must(`save did not write a version: ${(r1.problems || []).join('; ')}`, r1.file && fs.existsSync(r1.file)); + must('save did not name the newest walk with steps', r1.value?.run === 'runs/R-2026-09-25T08-41-12Z-zs8fns'); + const again = saveVersion(tmp, 'handle-inbound-lead', 'before', { by }); + must('save rewrote a version', again.problems?.some(p => p.includes('never rewritten'))); + const wrong = saveVersion(tmp, 'handle-inbound-lead', 'other', { by, run: 'R-2026-09-25T08-39-40Z-on0ces' }); + must('save named a walk of another journey', wrong.problems?.some(p => p.includes('not handle-inbound-lead'))); + const never = saveVersion(tmp, 'checkout', 'v1', { by }); + must('save named a journey never walked', never.problems?.some(p => p.includes('run it first'))); + + // pruning keeps what a version names + const runs = path.join(tmp, '.uxcli', 'runs'); const src = path.join(runs, 'R-2026-09-25T08-41-12Z-zs8fns'); + const made = []; + for (let i = 0; i < 8; i++) { + const name = `R-2026-10-0${1 + Math.floor(i / 4)}T0${i % 4}-00-00Z-p${String(i).padStart(5, '0')}`; + const dir = path.join(runs, name); fs.mkdirSync(dir, { recursive: true }); + const run = JSON.parse(fs.readFileSync(path.join(src, 'run.json'), 'utf8')); run.ranAt = `2026-10-0${1 + Math.floor(i / 4)}T0${i % 4}:00:00Z`; + fs.writeFileSync(path.join(dir, 'run.json'), JSON.stringify(run)); made.push(dir); + } + const oldest = made[0]; + const keepNamed = fs.mkdtempSync(path.join(os.tmpdir(), 'uxcli-version-')); + fs.cpSync(tmp, keepNamed, { recursive: true }); + // must-fail: nothing names the oldest walk, so pruning to seven removes it + prune(tmp, { keep: 7, pinned: pinnedRuns(tmp) }); + must('pruning kept a walk nothing names', !fs.existsSync(oldest)); + // must-pass: a version names it, so it stays + saveVersion(keepNamed, 'handle-inbound-lead', 'pinned', { by, run: path.basename(oldest) }); + prune(keepNamed, { keep: 7, pinned: pinnedRuns(keepNamed) }); + must('pruning removed a walk a version names', fs.existsSync(path.join(keepNamed, '.uxcli', 'runs', path.basename(oldest)))); + + // two versions, compared + const c = await versionComparison(EXAMPLE, 'handle-inbound-lead', 'v1', 'v2'); + must(`the example versions did not compare: ${(c.problems || []).join('; ')}`, !!c.entry); + if (c.entry) { + must('the comparison did not carry the steps that moved', c.entry.change.totals.steps.delta === c.entry.report.totals.steps - c.entry.previous.report.totals.steps); + const out = experiencePage([c.entry], path.join(tmp, 'experience', 'index.html')); + const html = fs.readFileSync(out, 'utf8'); + must('the page does not lay the two versions side by side', /class="compare"/.test(html) && (html.match(/class="pair"/g) || []).length === c.entry.report.steps.length && html.includes('v1') && html.includes('v2')); + } + const missing = await versionComparison(EXAMPLE, 'handle-inbound-lead', 'v1', 'v9'); + must('a comparison with a version that does not exist was not refused', missing.problems?.some(p => p.includes('v9'))); + for (const d of [tmp, keepNamed]) fs.rmSync(d, { recursive: true, force: true }); + return { ok: !problems.length, checks, problems }; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const r = await pair(); + console.log(r.ok ? `PASS version · ${r.checks} checks` : 'FAIL ' + r.problems.join('\n ')); + process.exit(r.ok ? 0 : 1); +}