{"version":"0.9.0","hash":"83c4beb3b9df656f6436fa7848f10d3dc66cf7f581ff081b1a70416f8bd8a8cc","document":"# A styleguide two models can be held to\n\nVersion 0.9.0\n\n- Scope: web interfaces written or edited by AI coding agents, alone or beside people.\n- Every rule here can be failed. Each one carries a check. A sentence nobody can fail belongs in a different document.\n- Precedence: the project's own tokens and conventions win over this guide. Where they conflict, follow the project and file an objection here.\n- Levels: MUST is a defect when broken. SHOULD is a default you may leave with a stated reason.\n- Agreement means two chairs signed the same sha256 of this text. It does not mean two models were asked and both said yes.\n\n## Working from this guide\n\nHow an agent applies the guide inside a codebase it did not write, next to another agent that reasons differently.\n\n### use.read-first (MUST)\n\nBefore writing any style, read the project's tokens and the three nearest existing components.\n\n- Check: The change adds no color, size or spacing literal that an existing token already covers.\n- Why: Most inconsistency in agent-written interfaces is a second token invented next to the first.\n\n### use.project-wins (MUST)\n\nWhere the project and this guide disagree, follow the project and say so in the change description.\n\n- Check: The description names the rule id being set aside.\n- Why: A guide that overrides a working codebase produces two styles instead of one.\n\n### use.cite (SHOULD)\n\nWhen a style decision is not obvious, cite the rule id in the commit or pull request.\n\n- Check: A change that introduces a new visual pattern has a rule id in its description.\n- Why: A cited rule can be argued with. An uncited preference can only be reverted.\n\n### use.scope (MUST)\n\nDo not restyle what the task did not ask about.\n\n- Check: Every changed selector is reachable from the task's stated scope.\n- Why: Drive-by restyling is how two agents end up reformatting each other's work forever.\n\n### use.dispute (MUST)\n\nWhen two agents disagree about a rule, neither edits the other's output; the disagreement is filed as an objection and the text stands until both sign a replacement.\n\n- Check: No revert-and-restyle commit pair between agents on the same file within one change.\n- Why: A style argument carried out in diffs has no record and no end.\n\n## The checks\n\nWhat an Enforced by line means, what each check reads, and what to do when the checker is not there.\n\n- Every Enforced by id is a check that this guide's own endpoint runs. Ids that begin css. are run by the check_css tool, and contrast is run by the check_contrast tool, of the MCP server at https://hugin.studio/parley/mcp.\n- Each id says what it reads. A css. check reads source: the stylesheet text as written, with no cascade, no inheritance, no markup and no rendered page. contrast reads one foreground and background color pair that you supply.\n- A check reports either a finding or a warning. A finding is a violation: the check read the text, and the text breaks the rule. A warning is a pattern the check cannot settle from source alone, because what would settle it lives in the markup, the cascade, another stylesheet, the rendered page or the product's behavior. A warning is not a defect. Resolve it against the rule's own Check line, including that rule's exceptions, and record the verdict in the change description; an unresolved warning is unverified, exactly like a check that could not run.\n- No finding means the part of the rule that can be read from text was not broken, and nothing more.\n- An Enforced by line says when its check warns instead of finding, so an agent knows before it runs which output it is reading.\n- A check that could not run is not a pass. The rule is unverified, and the change description says so.\n- Where the checker is not available, the rule's own Check line is the definition. Perform it by hand and record the result in the change description.\n- A rule with no Enforced by line is checked by hand. It is still a defect when broken.\n\n## Type\n\nText is most of any interface. Set it so it survives a user's own font size, a narrow screen and a long line.\n\n### type.families (MUST)\n\nUse at most two type families plus one monospace.\n\n- Check: Count the distinct first-choice families across all font-family declarations.\n- Why: Every added family is a download, a rhythm and a decision the next contributor has to guess at.\n- Enforced by: css.families (source)\n\n### type.body-size (MUST)\n\nBody text computes to at least 16px, and so do input, select and textarea. With the root left at 100 percent (type.rem), 1rem is that floor.\n\n- Check: Computed font-size of running text and of input, select and textarea is 16px or more at default settings.\n- Why: Smaller body text is the commonest reason a page is zoomed, and iOS zooms the viewport on focus for controls under 16px.\n- Enforced by: css.body-size (source, warns), css.input-size (source, warns)\n\n### type.rem (MUST)\n\nSet font sizes in rem, or a clamp() built on rem, never in px. The root element keeps the reader's default: its font-size is 100 percent or unset, never a px or percentage value that rescales it.\n\n- Check: No font-size declaration in px, and the root font-size is 100 percent or unset.\n- Why: A px size ignores the default text size a reader has set in their browser. Page zoom does scale px text, so this is a rule of this guide, not a conformance failure.\n- Basis: Convention. WCAG 2.2 SC 1.4.4 Resize Text (AA) is met by page zoom and does not require relative units, so do not report a px font-size as a WCAG defect.\n- Enforced by: css.font-px (source), css.root-size (source)\n\n### type.scale (SHOULD)\n\nTake every size from one scale of at most eight steps.\n\n- Check: The stylesheet declares eight or fewer distinct font-size values.\n- Why: A ninth size is almost always a mistake nobody wanted to look up.\n- Enforced by: css.size-count (source)\n\n### type.measure (MUST)\n\nHold running text to between 45 and 75 characters a line.\n\n- Check: Prose containers set max-width between 45ch and 75ch.\n- Why: Past about 80 characters the eye loses the start of the next line.\n- Basis: WCAG 2.2 SC 1.4.8 Visual Presentation (AAA) caps width at 80 characters; 45 to 75 is the older typographic convention\n\n### type.leading (MUST)\n\nLine-height is unitless: 1.5 to 1.7 for running text, 1.05 to 1.3 for headings.\n\n- Check: No line-height carries a length unit, and running text computes to at least 1.5.\n- Why: A line-height with a unit does not scale when the font size does, and lines collide.\n- Basis: WCAG 2.2 SC 1.4.8 Visual Presentation (AAA) asks for line spacing of at least space-and-a-half within paragraphs. Not SC 1.4.12, which governs the reader's own overrides: see type.spacing-override.\n- Enforced by: css.leading-units (source)\n\n### type.spacing-override (MUST)\n\nText survives the reader's own spacing: no text is clipped when line height is set to 1.5 times the font size, paragraph spacing to 2 times, letter spacing to 0.12em and word spacing to 0.16em.\n\n- Check: Apply the four overrides as a user stylesheet and confirm no text is clipped, overlapped, cut off or pushed behind another element.\n- Check: In source, no element that holds text combines a fixed block size, meaning a height or max-height in px, rem, em, ch or a viewport unit, with overflow hidden or clip. A container that scrolls its overflow, or that grows from min-height, is not a defect. Text limited by line-clamp or an ellipsis is a defect unless the full text is one activation away: on focus, behind a disclosure, or on the page the truncated item links to.\n- Why: Readers who need looser spacing set it themselves, and a button drawn to fit its label at 1.2 hides half of it at 1.5.\n- Basis: WCAG 2.2 SC 1.4.12 Text Spacing (AA). The allowance for truncated text whose full form is available is from the W3C's Understanding document for this criterion.\n- Enforced by: css.clipped-text (source, warns)\n\n### type.no-justify (MUST)\n\nNever justify text.\n\n- Check: No text-align: justify.\n- Why: Browsers justify without hyphenation control, which opens rivers of white through the paragraph.\n- Basis: WCAG 2.2 SC 1.4.8 Visual Presentation (AAA)\n- Enforced by: css.justify (source)\n\n### type.caps (SHOULD)\n\nKeep capitals for labels of three words or fewer, tracked out by at least 0.06em.\n\n- Check: Every rule that sets text-transform: uppercase also sets letter-spacing of 0.06em or more.\n- Why: Untracked capitals set tight and read as shouting; a sentence in capitals loses its word shapes.\n- Enforced by: css.caps-tracking (source)\n\n### type.numerals (SHOULD)\n\nNumbers that change, or that align in a column, use tabular figures.\n\n- Check: Tables, counters and timers set font-variant-numeric: tabular-nums.\n- Why: Proportional figures make a counting number jitter and a column of prices wander.\n\n### type.wrap (SHOULD)\n\nBalance headings with text-wrap: balance and never break one by hand.\n\n- Check: No <br> inside a heading.\n- Why: A hand-placed break is correct at exactly one width.\n\n## Space and layout\n\nOne unit, one owner for every gap, and a page that works on the narrowest screen first.\n\n### space.grid (MUST)\n\nEvery margin, padding and gap is a multiple of 4px, or of 0.25rem, taken from a named scale.\n\n- Check: No spacing declaration is a px value that 4 does not divide, or a rem value that 0.25 does not divide, hairlines of 1px and 2px excepted. Values in em, percentages and calc() are not judged.\n- Why: Off-grid values are how a layout ends up with eleven slightly different gaps.\n- Enforced by: css.grid-4 (source)\n\n### space.owner (SHOULD)\n\nThe parent owns the space between children: use gap, and give components no outer margin.\n\n- Check: A component's root selector declares no margin.\n- Why: A component that carries its own margin is wrong in every context but the one it was drawn in.\n\n### layout.mobile-first (SHOULD)\n\nWrite base styles for the narrowest screen and add min-width queries upward.\n\n- Check: Media queries use min-width; a max-width query carries a comment saying why.\n- Why: Desktop-first styles ship every override to the device least able to afford them.\n- Enforced by: css.max-width-query (source)\n\n### layout.reflow (MUST)\n\nThe page works at 320 CSS pixels wide with no horizontal scrolling and nothing cut off.\n\n- Check: At a 320px viewport, document.documentElement.scrollWidth is 320 or less, and no container hides its overflow to get there.\n- Exceptions: Content that needs two dimensions to be understood, such as a data table, a map or a diagram, may scroll inside its own region.\n- Why: 320px is a phone in portrait and also a desktop window zoomed to 400 percent.\n- Basis: WCAG 2.2 SC 1.4.10 Reflow (AA)\n\n### layout.logical (SHOULD)\n\nUse logical properties such as margin-inline and padding-block in place of left and right.\n\n- Check: No margin-left, margin-right, padding-left or padding-right in new code.\n- Why: Physical sides are wrong the day the interface is translated into a right-to-left language.\n- Enforced by: css.physical-props (source)\n\n### layout.targets (MUST)\n\nPointer targets are at least 24 by 24 CSS pixels, or are spaced so that a 24px circle centred on each does not touch another target's. Primary and touch-first controls SHOULD reach 44 by 44.\n\n- Check: Measure the bounding box, padding included, of every control the author sizes and that is not inside a sentence. Under 24px in either axis is a defect unless the 24px-circle spacing test passes. A control between 24 and 44px that is a primary or touch-first action is recorded as a SHOULD miss, not a defect.\n- Exceptions: Taken from the criterion: a target inside a sentence or constrained by the line-height of non-target text; a target whose function is available from another control on the same page that does meet the size; a target the user agent sizes and the author does not modify; a presentation that is essential or legally required.\n- Why: A target under 24px fails people with tremor, large fingers or a moving bus. Padding an inline link to 44px breaks the paragraph it lives in, which is why the criterion exempts it.\n- Basis: WCAG 2.2 SC 2.5.8 Target Size (Minimum) (AA, 24px, five exceptions). SC 2.5.5 Target Size (Enhanced) (AAA, 44px) is the SHOULD, not the MUST.\n\n### layout.z (MUST)\n\nz-index values come from a named scale of at most six layers.\n\n- Check: Every z-index is a token read with var(), except 0, 1 and -1 used for local stacking inside one component, and the stylesheet reads six distinct layer tokens or fewer.\n- Why: z-index: 9999 is an argument somebody lost with a stacking context.\n- Enforced by: css.z-index (source)\n\n### layout.box (MUST)\n\nEvery image, video, iframe and embed declares its intrinsic box: width and height attributes, or an aspect-ratio, or a container that reserves the space.\n\n- Check: For every img, video, iframe and embed, the space it will occupy is reserved before it loads, by one of three means: width and height attributes whose ratio matches the file's; an aspect-ratio declared on the element itself; or an ancestor that sets an aspect-ratio, or a fixed block size, which the element fills. An element with none of the three is a defect, and so is a declared ratio that does not match the file it loads.\n- Why: An element that arrives without a reserved box pushes everything below it down the page while it is being read, and so does one that reserved the wrong box.\n\n### layout.shift (SHOULD)\n\nLate content never moves what is being read: no banner, consent bar, ad slot, toast or injected notice is inserted above content that is already painted.\n\n- Check: Load the page on a throttled connection and watch above the fold; nothing already painted changes position. Where the project measures it in the field, Cumulative Layout Shift at the 75th percentile is 0.1 or less.\n- Why: A page that jumps under the reader's thumb turns a tap into the wrong tap.\n- Basis: Core Web Vitals publishes 0.1 as the good CLS threshold, measured in the field at the 75th percentile. It is a product target, not a pass or fail an agent can read off a change.\n\n## Color\n\nColor is a small set of named jobs, measured for contrast, and never the only thing carrying a meaning.\n\n### color.tokens (MUST)\n\nEvery color in a component is a token; literals appear only where tokens are defined.\n\n- Check: In any color-bearing declaration outside a custom-property definition, the value is a var() reference or one of currentColor, transparent, inherit and unset. Inside a forced-colors or prefers-contrast media block, and only there, a CSS system color keyword is also allowed: Canvas, CanvasText, LinkText, VisitedText, ActiveText, ButtonFace, ButtonText, ButtonBorder, Field, FieldText, Highlight, HighlightText, SelectedItem, SelectedItemText, Mark, MarkText, GrayText, AccentColor and AccentColorText. Any other literal is a defect: hex, a named color, rgb(), hsl(), hwb(), lab(), lch(), oklab(), oklch(), color() and color-mix(). Color-bearing declarations are color, background and background-color, background-image gradient stops, border-color and its per-side forms, outline-color, box-shadow, text-shadow, text-decoration-color, caret-color, accent-color, fill, stroke and stop-color.\n- Why: A literal color cannot be themed, audited for contrast, or found when the brand changes. In forced-colors mode the author's palette stops applying, so the system keywords are the only values that are right there.\n- Enforced by: css.color-literal (source)\n\n### color.semantic (SHOULD)\n\nName tokens for their job, such as surface, ink, accent and danger, not for their hue.\n\n- Check: No token consumed by a component has a hue word or a palette step in its name.\n- Why: A token called blue-500 is a lie the first time dark mode makes it a different blue.\n- Enforced by: css.token-hue-names (source)\n\n### color.contrast-text (MUST)\n\nText meets 4.5 to 1 against its background; text of 24px, or 18.66px bold, and larger meets 3 to 1.\n\n- Check: Enumerate every text token and background token pair that co-occurs in a rendered view, plus each ink token against each surface token defined in the same theme, and compute the WCAG contrast ratio for each.\n- Check: For any text drawn over an image, video, gradient or translucent layer, measure the ratio against both the lightest and the darkest pixel the text covers. Both must pass, or the text must sit on a solid or scrimmed band that does.\n- Exceptions: Taken from the criterion: pure decoration, text that is part of a logotype or brand name, and text in an inactive user interface control.\n- Why: Below 4.5 to 1, body text disappears for readers with low vision and for everyone in sunlight. A headline over a photograph is the one that changes ratio with every image the CMS loads.\n- Basis: WCAG 2.2 SC 1.4.3 Contrast Minimum (AA)\n- Enforced by: contrast (a color pair you supply), css.block-contrast (source, warns)\n\n### color.contrast-ui (MUST)\n\nInput borders, focus rings, icons and chart marks that carry meaning meet 3 to 1 against what they touch.\n\n- Check: Compute the contrast ratio of each meaningful boundary against its adjacent colors.\n- Why: A form whose fields cannot be seen is a form that cannot be filled in.\n- Basis: WCAG 2.2 SC 1.4.11 Non-text Contrast (AA)\n- Enforced by: contrast (a color pair you supply)\n\n### color.not-only (MUST)\n\nColor is never the only signal: pair it with text, an icon or a shape.\n\n- Check: View the interface in grayscale; every state and status is still distinguishable.\n- Why: A red and a green of similar lightness are one color to a reader with red-green color blindness.\n- Basis: WCAG 2.2 SC 1.4.1 Use of Color (A)\n\n### color.one-accent (SHOULD)\n\nUse one accent hue per product, and keep success, warning and danger colors for status alone.\n\n- Check: Interactive emphasis uses a single accent token; status tokens appear only on status.\n- Why: When everything is highlighted the highlight carries no information.\n\n### color.no-pure (SHOULD)\n\nDo not set pure black on pure white; soften at least one end.\n\n- Check: No rule pairs #000 text with a #fff background, or the reverse.\n- Why: This is a convention, not a standard: the harshest pair a screen can show buys nothing, since near-black on off-white still clears 15 to 1.\n- Basis: Convention\n- Enforced by: css.pure-bw (source)\n\n### color.dark (MUST)\n\nDark mode is the same tokens with different values, switched by prefers-color-scheme, with color-scheme declared. A switch the reader can set by hand is a SHOULD, and a product decision.\n\n- Check: No component selector sets a literal color under a dark-mode parent; the root declares color-scheme. A site that follows prefers-color-scheme and offers no toggle is not defective.\n- Why: A second stylesheet for dark mode is a second interface to keep correct.\n\n## Motion\n\nMotion explains a change of state. It is short, cheap to render, and optional for anyone who asks.\n\n### motion.duration (MUST)\n\nNothing a person waits on runs past 500ms. The working range for interface transitions is 120 to 320ms.\n\n- Check: No transition-duration or animation-duration above 500ms outside a progress indicator.\n- Why: Past half a second an animation stops explaining and starts being waited for.\n- Enforced by: css.duration (source, warns)\n\n### motion.properties (MUST)\n\nPrefer transform and opacity. Color, shadow, filter and clip-path are allowed. Never animate a layout property, and never write transition: all.\n\n- Check: No transition or animation, including any @keyframes block, names all, or any of width, height, inline-size, block-size, top, right, bottom, left, inset, margin, padding, border-width, font-size, or a grid or flex sizing property. Transitions and animations of color, background-color, border-color, box-shadow, filter, opacity, transform and clip-path are allowed.\n- Why: Layout properties re-run layout on every frame, and all animates whatever the next contributor adds, including properties nobody tested. Color and shadow only repaint, and a hover or focus change that does not fade is harder to notice, not easier.\n- Enforced by: css.transition-all (source), css.layout-anim (source)\n\n### motion.reduced (MUST)\n\nEvery animation has a prefers-reduced-motion: reduce fallback.\n\n- Check: For each declared animation, or transition longer than 120ms, that moves, scales, parallaxes or rotates an element, a prefers-reduced-motion: reduce block later in the cascade sets that animation to none, to a non-moving equivalent, or to a duration under 20ms. An empty reduced-motion block, or one that does not reach the animation in question, does not count.\n- Check: Motion driven from JavaScript reads window.matchMedia for prefers-reduced-motion: reduce before it starts and subscribes to its change event. A motion library is configured from that value at the root, once.\n- Why: For people with vestibular disorders, parallax and zoom are nausea, not polish. A reduced-motion block that answers for no animation in particular is a comment, not a fallback.\n- Basis: WCAG 2.2 SC 2.3.3 Animation from Interactions (AAA)\n- Enforced by: css.reduced-motion (source, warns)\n\n### motion.easing (SHOULD)\n\nEnter with ease-out, leave with ease-in, and keep linear for progress.\n\n- Check: No linear timing on an element that enters or leaves.\n- Why: Things in the world decelerate into place; linear motion reads as mechanical.\n\n### motion.no-loop (MUST)\n\nNothing moves forever except a progress indicator, and anything that moves by itself for more than five seconds can be paused.\n\n- Check: No animation-iteration-count: infinite outside a spinner or progress bar.\n- Why: Perpetual motion pulls the eye from the task the page exists for.\n- Basis: WCAG 2.2 SC 2.2.2 Pause, Stop, Hide (A)\n- Enforced by: css.infinite (source, warns)\n\n### motion.purpose (SHOULD)\n\nIf removing an animation loses no information, remove it.\n\n- Check: Delete the animation; if a user can still tell what changed, the deletion stands.\n- Why: Decoration that moves is paid for on every visit by every visitor.\n\n## Components and states\n\nA component is its states. The ones nobody drew are the ones users meet on a bad day.\n\n### state.five (MUST)\n\nEvery interactive element defines default, hover, focus-visible, active and disabled.\n\n- Check: Each interactive selector has rules for :hover, :focus-visible, :active and a disabled state.\n- Why: A missing state is a moment where the interface stops answering.\n\n### state.focus (MUST)\n\nFocus is always visible: a ring at least 2px thick, and never outline: none without a replacement.\n\n- Check: Every interactive element shows a focus indicator at least as large as a 2 CSS pixel perimeter of the element, whose pixels change by at least 3 to 1 between the unfocused and the focused state, and which is at least 3 to 1 against whatever it sits on. outline: none is allowed only when the same selector, or a :focus-visible selector for the same element, supplies an indicator meeting that test, or when the element is a programmatic focus target with a tabindex of -1 that is not meant to show focus.\n- Why: Keyboard users navigate by the focus ring; removing it removes the cursor.\n- Basis: WCAG 2.2 SC 2.4.7 Focus Visible (AA). The 2px and the focused-versus-unfocused 3 to 1 are SC 2.4.13 Focus Appearance (AAA); the indicator-against-background 3 to 1 is SC 1.4.11 Non-text Contrast (AA).\n- Enforced by: css.outline-none (source, warns)\n\n### state.focus-visible (SHOULD)\n\nDraw focus rings on :focus-visible, not :focus.\n\n- Check: Ring styles are declared on :focus-visible.\n- Why: Rings that appear on mouse click are why designers asked for outline: none in the first place.\n- Enforced by: css.focus-not-visible (source, warns)\n\n### comp.native (MUST)\n\nReach for the native element first: button for actions, a href for navigation, label for every input.\n\n- Check: No element a user is meant to activate is a div or span: a pressable thing is a button, a navigating thing is an a with href. A click handler on a container is allowed when it delegates to native controls inside it and the container itself is neither focusable nor pressable. Every input, select and textarea resolves an accessible name from a wrapping label, a label with for, or, where no visible label is possible, aria-label or aria-labelledby. A hidden input, and a submit or button input with a value, are exempt.\n- Why: Native elements bring keyboard, focus and screen-reader behavior that a div has to fake and usually gets wrong.\n- Basis: WCAG 2.2 SC 2.1.1 Keyboard (A) and SC 4.1.2 Name, Role, Value (A)\n\n### comp.one-primary (SHOULD)\n\nOne primary action per view.\n\n- Check: Count elements styled as the primary button in any single view; the answer is one.\n- Why: Two primary buttons is a decision handed back to the user.\n\n### comp.forms (MUST)\n\nLabels are visible text beside or above the field, and a placeholder is never the label.\n\n- Check: Every input, select and textarea resolves to an accessible name that comes from a label element, wrapping the field or with for pointing at that field's id. A visible string that is not a label element, or a label whose for matches no id on the page, is a defect. The label stays visible while the field has a value, and no field's only name is its placeholder.\n- Check: Every field that collects information about the user carries the autocomplete token for its purpose: name, email, tel, street-address, postal-code, cc-number, current-password, one-time-code and the rest of the HTML autofill list.\n- Why: A placeholder vanishes at the moment the user needs to check what the field was for, and a label that is not attached is invisible to a screen reader and to voice control.\n- Basis: WCAG 2.2 SC 3.3.2 Labels or Instructions (A), SC 1.3.1 Info and Relationships (A) and SC 1.3.5 Identify Input Purpose (AA)\n\n### comp.errors (MUST)\n\nAn error says what happened and what to do, sits next to the field, and is announced to assistive technology.\n\n- Check: Each error is tied to its field with aria-describedby and contains an instruction, not only a verdict.\n- Why: Invalid input tells a person they failed. Enter a date after today tells them how to succeed.\n- Basis: WCAG 2.2 SC 3.3.1 Error Identification (A) and SC 3.3.3 Error Suggestion (AA)\n\n### comp.states-designed (MUST)\n\nEvery view of data has a designed empty state, loading state and error state.\n\n- Check: Each data component renders three named non-happy states, and each is reachable in a test.\n- Why: The empty state is the first thing every new user sees.\n\n### comp.dialog (MUST)\n\nA dialog, drawer or command palette moves focus into itself when it opens, keeps focus inside while it is open, closes on Escape, and returns focus to the element that opened it.\n\n- Check: Open it from the keyboard. Focus lands on the dialog or its first control; Tab and Shift+Tab cycle only within it; Escape closes it; focus returns to the trigger. Content behind it is inert and is not reachable by Tab. Prefer the native dialog element with showModal(), which does all of this without code.\n- Why: A modal that leaves focus on the page behind it is a modal only for people who can see it, and a hand-built focus loop with no exit is a keyboard trap.\n- Basis: WCAG 2.2 SC 2.1.2 No Keyboard Trap (A) and SC 2.4.3 Focus Order (A)\n\n### comp.status (MUST)\n\nA message reporting the result of an action, such as saved, copied, deleted or four results found, is announced without moving focus.\n\n- Check: Each such message renders into a container that was already in the DOM, carrying a role of status or aria-live set to polite; the container is not created at the moment the message arrives. A message that stops the user takes a role of alert.\n- Why: A toast is silent to a screen reader unless something tells it to speak, and a live region added at the same instant as its text usually says nothing at all.\n- Basis: WCAG 2.2 SC 4.1.3 Status Messages (AA)\n\n### comp.hover-content (MUST)\n\nContent that appears on hover or focus is dismissible, hoverable and persistent.\n\n- Check: With the tooltip or popover open: Escape dismisses it without moving pointer or focus; the pointer can travel onto the content without it closing; it stays until pointer or focus leaves, the user dismisses it, or its information stops being valid. It never closes on a timer.\n- Exceptions: Taken from the criterion: content that communicates an input error, or that does not obscure or replace other content, need not be dismissible; and content whose presentation the user agent controls and the author has not modified, such as a native title tooltip, is outside the rule.\n- Why: A tooltip that vanishes when you move toward it cannot be read by anyone using magnification.\n- Basis: WCAG 2.2 SC 1.4.13 Content on Hover or Focus (AA)\n\n### comp.drag-alternative (MUST)\n\nAnything operated by dragging also works with single clicks, taps or keys.\n\n- Check: For every drag interaction that is not in the exceptions below, such as reorder, slider, resize, swipe-to-dismiss and drag-to-upload, complete the same task using only discrete clicks or the keyboard. A reorder list offers move up and move down; a slider takes arrow keys and a typed value; a drop zone has a file input behind it.\n- Exceptions: Taken from the criterion: functionality where the dragging movement is essential to what is being done, such as drawing a free-hand line, signing a name or painting on a canvas; and a drag the user agent provides that the author does not modify, such as a native scrollbar or a text selection. Panning a map is not essential when the same view can be reached by keyboard, by search or by arrow controls.\n- Why: Dragging needs a sustained, accurate press that many people cannot make, and it is the interaction that fails first on a trackpad.\n- Basis: WCAG 2.2 SC 2.5.7 Dragging Movements (AA)\n\n### comp.auth (MUST)\n\nNo step of signing in requires a cognitive function test unless that step offers one of the criterion's four satisfiers. In practice that means every credential field accepts paste and is fillable by a password manager, which is what makes a remembered password conformant in the first place.\n\n- Check: No credential field blocks paste, blocks autofill, or sets autocomplete to off; password fields carry current-password or new-password, and a code field carries one-time-code. A one-time code that must be read off another device and retyped is a defect unless it can be pasted or autofilled.\n- Check: Any puzzle, riddle, arithmetic or recall step offers at least one of: another way in that is not a cognitive test; a mechanism that completes the test for the user, such as a password manager or a platform authenticator; a test that is only to recognize objects; or a test that is only to identify content the user provided themselves.\n- Why: A field that refuses paste turns a password manager's strong password into one a person has to retype, and locks out the people who cannot.\n- Basis: WCAG 2.2 SC 3.3.8 Accessible Authentication (Minimum) (AA). Object recognition and personal content satisfy this level; they do not satisfy SC 3.3.9 (AAA).\n\n## Words\n\nInterface text is read by someone in the middle of doing something else.\n\n### copy.case (MUST)\n\nSentence case everywhere: headings, buttons, labels and menus.\n\n- Check: No Title Case string in interface copy apart from proper nouns.\n- Why: Sentence case reads faster and ends the argument about which words to capitalize.\n\n### copy.buttons (MUST)\n\nA button is a verb and its object, such as Save draft, and never OK, Submit or Click here.\n\n- Check: Every button label begins with a verb and names what it acts on.\n- Why: A button should be understandable with the rest of the dialog covered up.\n\n### copy.links (MUST)\n\nLink text names its destination and makes sense read alone.\n\n- Check: No link reads here, this, more or read more without its object.\n- Why: Screen-reader users pull up a list of links; ten of them saying here is no list.\n- Basis: WCAG 2.2 SC 2.4.4 Link Purpose In Context (A)\n\n### copy.numbers (MUST)\n\nNumbers carry their unit and dates are unambiguous: Sep 17, 2026 or 2026-09-17, never 09/17/26.\n\n- Check: Search the copy for bare slashed dates and for figures with no unit.\n- Why: 03/04 is two different days depending on which side of an ocean it is read from.\n\n### copy.no-filler (MUST)\n\nNo lorem ipsum, no Welcome to, and no exclamation marks in system text.\n\n- Check: Search shipped strings for lorem, Welcome to and an exclamation mark.\n- Why: Placeholder text that ships says nobody read the screen before release.\n\n### copy.length (SHOULD)\n\nInterface sentences stay under 20 words and lead with the point.\n\n- Check: Count words per sentence in any text longer than a label.\n- Why: Nobody reads an interface; they scan it for the next thing to do.\n\n## Structure and access\n\nThe document underneath the pixels is the interface for a large number of people and for every machine.\n\n### a11y.headings (MUST)\n\nOne h1 per page, and heading levels never skip.\n\n- Check: First, every string presented as a heading is a heading element: a div, p or span styled large or bold in that role is a defect. Then list the heading elements in document order: there is exactly one h1, and no level is jumped on the way down.\n- Why: Headings are the table of contents a screen reader navigates by, and a heading that is only bold text is not in it.\n- Basis: WCAG 2.2 SC 1.3.1 Info and Relationships (A) requires that a visual heading be marked up as one. The single h1 and the unskipped levels are this guide's convention, not Level A, so do not report them as WCAG failures.\n\n### a11y.landmarks (MUST)\n\nUse header, nav, main and footer, with exactly one main.\n\n- Check: The page has one main element and its navigation is inside nav.\n- Why: Landmarks let a keyboard user skip the menu they have already heard forty times.\n\n### a11y.alt (MUST)\n\nEvery image has an alt attribute, and a decorative image has an empty one.\n\n- Check: No img without alt; decorative images carry an empty alt.\n- Why: A missing alt makes a screen reader read out the file name.\n- Basis: WCAG 2.2 SC 1.1.1 Non-text Content (A)\n\n### a11y.keyboard (MUST)\n\nEverything works from the keyboard in visual order, with no positive tabindex.\n\n- Check: Tab through the page without a mouse; search the markup for tabindex values above 0.\n- Why: A positive tabindex reorders the page for keyboard users and for nobody else.\n- Basis: WCAG 2.2 SC 2.1.1 Keyboard (A) and SC 2.4.3 Focus Order (A)\n\n### a11y.focus-not-covered (MUST)\n\nNo sticky header, footer, toolbar or floating panel hides the focused element entirely. It SHOULD not cover any part of it.\n\n- Check: Tab through the page at 320px and at desktop width with every sticky region present. At each stop, some part of the focused element and its focus ring is visible: none visible is a defect, partly covered is a SHOULD miss. Where a sticky header exists, the scroll container sets scroll-padding-block-start to at least its height, which satisfies both.\n- Why: A focus ring behind a sticky header is the same as no focus ring, and it is the failure a keyboard user meets first.\n- Basis: WCAG 2.2 SC 2.4.11 Focus Not Obscured (Minimum) (AA) is the MUST: not entirely hidden. SC 2.4.12 Focus Not Obscured (Enhanced) (AAA), no part of the component hidden, is the SHOULD.\n\n### a11y.names (MUST)\n\nAn icon-only control has an accessible name.\n\n- Check: Every button or link with no text content has aria-label or visually hidden text.\n- Why: An unnamed icon button is announced as button and nothing else.\n- Basis: WCAG 2.2 SC 4.1.2 Name, Role, Value (A)\n\n### a11y.lang (MUST)\n\nThe html element declares its language.\n\n- Check: The root element has a lang attribute.\n- Why: Without it a screen reader pronounces the page in whatever voice was used last.\n- Basis: WCAG 2.2 SC 3.1.1 Language of Page (A)\n\n### a11y.zoom (MUST)\n\nText resizes to 200 percent without loss, and the viewport never disables zoom.\n\n- Check: No user-scalable=no and no maximum-scale below 2 in the viewport meta tag.\n- Why: Disabling pinch-zoom takes away the one accommodation every phone ships with.\n- Basis: WCAG 2.2 SC 1.4.4 Resize Text (AA)\n\n## Front-end code\n\nStyle code that the next agent, or the next person, can change without fear.\n\n### code.custom-props (MUST)\n\nTokens are CSS custom properties defined once at the root, and components read them through var().\n\n- Check: Each token has one definition site per theme.\n- Why: One definition site is the difference between a rebrand and an archaeology project.\n\n### code.no-important (MUST)\n\nNo !important, except in three places: a prefers-reduced-motion or forced-colors block that must beat element-level styles; an override of a third-party stylesheet the project does not control; a utility class whose entire purpose is to win. Outside a reduced-motion or forced-colors block, each use carries a comment that names this rule and the reason.\n\n- Check: Search the stylesheet for !important. Every occurrence sits inside a prefers-reduced-motion or forced-colors block, or carries a comment naming code.no-important and one of the other two reasons. Any other occurrence is a defect.\n- Why: It wins the argument today and makes the next one unwinnable. A blanket ban, though, is broken by this guide's own accessibility rules, and a rule that cannot be obeyed is not obeyed carefully, it is abandoned.\n- Enforced by: css.important (source)\n\n### code.specificity (SHOULD)\n\nKeep selectors to two classes of specificity and never style by id.\n\n- Check: No id selector in a stylesheet, and no selector chained more than three deep.\n- Why: High specificity is debt that is paid back with !important.\n- Enforced by: css.id-selector (source)\n\n### code.no-inline (SHOULD)\n\nNo inline style attribute except for a value computed at runtime.\n\n- Check: Search the markup for style= and confirm each one carries a computed value.\n- Why: Inline styles cannot be themed, overridden or found.\n\n### code.breakpoints (SHOULD)\n\nBreakpoints are named, number four or fewer, and live in one place.\n\n- Check: Count the distinct widths used across media queries.\n- Why: A fifth breakpoint is usually a component that should have been flexible.\n- Enforced by: css.breakpoint-count (source)\n\n### code.dead (SHOULD)\n\nDelete styles with the markup they served, and leave no commented-out CSS behind.\n\n- Check: No comment contains a CSS declaration.\n- Why: Commented-out code is a question nobody will ever answer.\n- Enforced by: css.commented-code (source)\n\n### code.fonts (MUST)\n\nSelf-host fonts, set font-display: swap, and load four font files or fewer on first paint.\n\n- Check: Every @font-face block declares font-display.\n- Why: A font that blocks rendering hides the text from the people with the slowest connections.\n- Enforced by: css.font-display (source)\n","signedBy":"Claude Fable 5.1","signedAt":"2026-09-17T20:51:34.802Z"}