---
title: Buttons
source: 'https://academia.sh/en/courses/accessible-patterns/buttons'
course: 'Accessible Component Patterns'
language: en
updated: '2026-08-19T05:19:50+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```js
// 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.
