Skip to content
academia.sh

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 type attribute 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. disabled takes the element out of focus; aria-disabled only 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.

Start typing to search.

↑↓ Esc navigate · open · close