Lesson 01 / 25
Buttons
The six sections of a component specification, the behavior the native element provides for free, the pressed-state declaration, the keyboard contract, and auditing the clickable area against target size criteria.
Contents
The component catalog is established, the naming convention is written, and versioning and contribution are working. Every entry in the catalog carries a name, a maturity level, and a usage guideline. But a component’s presence in the catalog does not mean what it does is written down: the “primary button” entry says how the button looks, not how it is operated from the keyboard, or which name and which state it presents in the accessibility tree.
This course writes that missing half. Each lesson treats a component as a specification and answers the same six questions in the same order. The library catalog interface — the search field, the filter selections, the results table, the record detail, and the borrowing form — is the shared example across every lesson. The first component is the button, because most of the remaining components contain one.
The Six Sections of a Specification
A component specification is written in this order: the problem the component solves and when it is not used; what the native counterpart is; where role, name, and state are computed from; the keyboard contract; measurable constraints; and the common mistake and how it is caught.
The order is not arbitrary. If the third section is reached before the first two are answered, a meaningless container gets chosen instead of a native element, and the remaining four sections are written on top of that wrong choice. This is why the first rule from the Frontend Quality course is also the specification’s second entry: ARIA is not used when a native element carrying the required meaning exists.
What the Button Does
The button is the control that starts an action on the page: running a search, clearing filters, adding a record to the borrowing list, submitting a form. Their shared property is that the address bar’s address does not change when they run.
Where a button is not used follows the same measure: if the user is going to another resource, the component is a link, not a button. Clicking a record in the catalog opens the record detail page; that is not an action, it is a navigation. The full rule for the distinction, and how its violation is caught, is in this topic’s last lesson.
The Native Element First
The counterpart is the button element, and its type attribute takes three values.
The submit value ties the element to form submission and is the value in effect
inside a form when the attribute is not written; the button value carries no default
action, and behavior is determined solely by the code attached to it; the reset value
returns fields to their initial values.
Skipping this distinction produces a concrete breakage: when a “Clear filters” button
placed inside the borrowing form does not carry type="button", the button submits the
form. A user who wants to clear a field ends up borrowing the record instead.
Beyond role, the native element brings five more things for free: focusability,
operability by the Enter and Space keys, notifying the tree of the disabled state, a
relationship to form submission, and adding its own name–value pair to the submission
list as a submitter. None of these come from writing a role onto a div element.
Role, Name, and State
Role comes implicitly from the button element; it is not declared separately.
The name is computed from the button’s content. In buttons that carry text, this works on its own. In icon-only buttons — the flagging and borrow buttons in the results table — the button ends up nameless if the icon’s alternative text is empty. The rule in the catalog specification is this: an icon button’s name is written on the button, not on the icon; the icon is presentational in every case.
Two state declarations belong to the button. The first is being disabled: the disabled
attribute both notifies the tree and takes the element out of focus. aria-disabled="true"
only notifies — the element still takes focus and remains clickable; this is chosen when
the user is meant to be able to find the button and read why it does not work, and the
click has to be ignored on the code side.
The second is the pressed state (aria-pressed): it declares whether a toggle button
is on or off. The “Only items on the shelf” filter in the catalog is such a button. This
state is separate from the transient active interaction state defined in the
Fundamentals of Interface Design course: one ends when the key is released, the other
persists until the user presses again. Visual design has to encode the two on separate
channels.
Keyboard Contract
| Key | Behavior |
|---|---|
Tab |
Brings focus to the button; every button is its own tab stop. |
Shift+Tab |
Returns to the previous tab stop. |
Enter |
Runs the button; triggers the moment the key is pressed. |
Space |
Runs the button; triggers when the key is released. |
The difference between the two keys’ trigger moments exists on its own in the native element and has to be reproduced in a hand-built button. The Space key triggering on release gives the user the option to hold the key down and back out of the button; the Enter key triggering on press is the expected behavior for form submission.
Focus stays on the button after it runs. If the button is being removed from the page — if the “Clear filters” button is hidden once no filters remain — where focus goes has to be specified; if it is not, focus falls back to the document root.
The Clickable Area Is Measured
A button’s visual size is not a design preference; it is a constraint tied to criteria. The 2.5.8 Target Size (Minimum) criterion requires every target to be at least 24×24 CSS pixels and defines five exceptions; the most useful of these is the spacing exception: a 24-pixel-diameter circle placed at the center of a target that stays small satisfies the criterion if it does not touch another target or another small target’s circle. The 2.5.5 Target Size (Enhanced) criterion raises the same measure to 44 pixels.
// target.mjs — auditing button target size against the 2.5.8 and 2.5.5 criteria // Catalog toolbar and results-table row: boxes in CSS pixels. // x,y top-left corner; w,h width and height. const INTERFACE = [ { name: "button#search", x: 16, y: 16, w: 88, h: 44 }, { name: "button#clear-filters", x: 112, y: 16, w: 140, h: 44 }, { name: "button#view-list", x: 600, y: 26, w: 24, h: 24 }, { name: "button#view-grid", x: 628, y: 26, w: 24, h: 24 }, { name: "a#record[K-118]", x: 60, y: 80, w: 220, h: 20 }, { name: "button#flag[K-118]", x: 700, y: 80, w: 20, h: 20 }, { name: "button#borrow[K-118]", x: 720, y: 80, w: 20, h: 20 }, ]; const MIN = 24; // 2.5.8 Target Size (Minimum), AA const ENHANCED = 44; // 2.5.5 Target Size (Enhanced), AAA const center = (t) => [t.x + t.w / 2, t.y + t.h / 2]; // Whether a 24 px diameter circle intersects a box function circleBoxIntersects(t, k) { const [cx, cy] = center(t); const nx = Math.max(k.x, Math.min(cx, k.x + k.w)); const ny = Math.max(k.y, Math.min(cy, k.y + k.h)); return Math.hypot(cx - nx, cy - ny) < MIN / 2; } // Two 24 px diameter circles intersecting: distance between centers < 24 function circleCircleIntersects(a, b) { const [ax, ay] = center(a); const [bx, by] = center(b); return Math.hypot(ax - bx, ay - by) < MIN; } const small = (t) => Math.min(t.w, t.h) < MIN; // Spacing exception: a small target's circle must not touch another // target or another small target's circle. function spacingException(t, all) { const collided = []; for (const b of all) { if (b === t) continue; const hit = small(b) ? circleCircleIntersects(t, b) : circleBoxIntersects(t, b); if (hit) collided.push(b.name); } return collided; } function audit(heading, ui) { console.log(heading); console.log("target size smallest edge 2.5.8 2.5.5 spacing exception"); for (const t of ui) { const edge = Math.min(t.w, t.h); const collided = small(t) ? spacingException(t, ui) : []; const passes258 = !small(t) || collided.length === 0; const passes255 = Math.min(t.w, t.h) >= ENHANCED; const note = !small(t) ? "not required" : collided.length === 0 ? "applied" : "collides: " + collided.join(", "); console.log( t.name.padEnd(24) + `${t.w}x${t.h}`.padEnd(9) + String(edge).padStart(14) + (passes258 ? " passes" : " FAILS").padEnd(8) + (passes255 ? " passes" : " FAILS").padEnd(8) + " " + note, ); } const remaining = ui.filter((t) => small(t) && spacingException(t, ui).length > 0); console.log(`2.5.8 violations: ${remaining.length} targets`); } audit("current toolbar and row controls:", INTERFACE); // Fix: inline row controls are raised to 24x24 and their spacing opened to 8 px. const FIXED = INTERFACE.map((t) => { if (t.name === "button#flag[K-118]") return { ...t, w: 24, h: 24, x: 696, y: 78 }; if (t.name === "button#borrow[K-118]") return { ...t, w: 24, h: 24, x: 728, y: 78 }; return t; }); console.log(""); audit("inline row controls at 24x24, spacing 8 px:", FIXED); // Solving without enlarging the target: the box stays the same, the spacing opens. const SPACING_ONLY = INTERFACE.map((t) => t.name === "button#borrow[K-118]" ? { ...t, x: 726 } : t, ); console.log(""); audit("boxes stay 20x20, only the spacing opens to 6 px:", SPACING_ONLY);
current toolbar and row controls: target size smallest edge 2.5.8 2.5.5 spacing exception button#search 88x44 44 passes passes not required button#clear-filters 140x44 44 passes passes not required button#view-list 24x24 24 passes FAILS not required button#view-grid 24x24 24 passes FAILS not required a#record[K-118] 220x20 20 passes FAILS applied button#flag[K-118] 20x20 20 FAILS FAILS collides: button#borrow[K-118] button#borrow[K-118] 20x20 20 FAILS FAILS collides: button#flag[K-118] 2.5.8 violations: 2 targets inline row controls at 24x24, spacing 8 px: target size smallest edge 2.5.8 2.5.5 spacing exception button#search 88x44 44 passes passes not required button#clear-filters 140x44 44 passes passes not required button#view-list 24x24 24 passes FAILS not required button#view-grid 24x24 24 passes FAILS not required a#record[K-118] 220x20 20 passes FAILS applied button#flag[K-118] 24x24 24 passes FAILS not required button#borrow[K-118] 24x24 24 passes FAILS not required 2.5.8 violations: 0 targets boxes stay 20x20, only the spacing opens to 6 px: target size smallest edge 2.5.8 2.5.5 spacing exception button#search 88x44 44 passes passes not required button#clear-filters 140x44 44 passes passes not required button#view-list 24x24 24 passes FAILS not required button#view-grid 24x24 24 passes FAILS not required a#record[K-118] 220x20 20 passes FAILS applied button#flag[K-118] 20x20 20 passes FAILS applied button#borrow[K-118] 20x20 20 passes FAILS applied 2.5.8 violations: 0 targets
The three runs give three separate pieces of information.
In the first run, the toolbar satisfies the criterion; the results table’s inline row controls do not. The distance between the two buttons’ centers is 20 pixels; 24 is required for the two circles not to intersect. The record link is also 20 pixels tall, but it satisfies the spacing exception, because there is no other target around it.
The second run applies the direct fix: once the boxes are raised to 24×24, the exception is not needed at all. The third run corrects a misreading of the criterion — enlarging the boxes is not mandatory. Raising the spacing to just 6 pixels alone, moving the centers to 26 pixels, satisfies the criterion. This is the route to use when a dense results table needs to keep its row height.
The 2.5.5 column shows “FAILS” for every icon button. This is not a violation: the 44-pixel measure is AAA level. The column’s presence in the specification shows in advance which buttons need to be enlarged if touch use is the target.
Hierarchy Is Not a Behavior Difference
The catalog toolbar uses three visual weights: the filled primary button (“Search”), the outlined secondary button (“Clear filters”), and the unfilled plain button (the view switchers). This distinction is entirely visual; the role, name, keyboard contract, and target size criterion are the same for all three.
The one rule that has to be written into the specification is that a view holds at most one primary button. Two primary buttons erase the “this is the recommended action” information that visual hierarchy carries. This rule does not come from a conformance criterion; it is the component-level counterpart of the contrast and emphasis decision from the Fundamentals of Interface Design course.
Common Mistake
The three most commonly produced defects and how they are caught are as follows.
The nameless icon button. A button carrying an icon with empty alternative text appears nameless in the tree. Catching it needs no tooling: every button’s accessible name on the page is listed, and the empty one is searched for. Automated checkers’ presence rule catches this.
The visible text and the name diverging. If the button reads “Borrow” while its
aria-label declaration says “Add,” the name a voice-interface user speaks matches no
element; the 2.5.3 Label in Name criterion forbids this. Catching it means checking
whether the visible text is contained within the accessible name.
The disabled button left without a reason. When the borrow button for a record that
is not on the shelf is turned off by writing disabled, the button drops out of focus
and the user cannot read why. Catching it is a keyboard tour: every control that is
visible on screen but cannot be found while tabbing through the page is in this state.
The specification’s decision, when the reason needs to be readable, is to declare the
button with aria-disabled and tie the reason with aria-describedby.
Summary
- A component specification has six sections: the problem and where it is not used, the native counterpart, role–name–state, the keyboard contract, measurable constraints, and the common mistake and how it is caught.
- A button starts an action; if the address changes, the component is not a button. When
the
typeattribute is not written, a button inside a form becomes a submit button. - The name is computed from the button’s content; in icon buttons the name is written on
the button.
disabledtakes the element out of focus;aria-disabledonly notifies and leaves it in focus. - The Enter key triggers on press, the Space key on release; a hand-built button has to reproduce this difference.
- 2.5.8 requires 24×24 CSS pixels from every target; the spacing exception can also be satisfied without enlarging the box, by raising the distance between centers above 24 pixels.
- The distinction between primary, secondary, and plain buttons is purely visual; a view holds at most one primary button.
Next Step
The button is a control that runs without taking anything from the user: its name is fixed, it carries no value, and the only thing it declares is whether it is pressed. The catalog interface’s actual data-collecting component is the search field, and there a name alone is not enough — the field has a label, a format hint, a help text, and, when needed, an error message. All four of these sit around the field on screen, but in the accessibility tree they are separate declarations, and the order in which they are announced differs. The next lesson specifies the text field through the arrangement of these four texts.
To keep your progress and take notes, Log in
My notes
Log in to take notes.