Testing Target Size 2.5.8 in Dense Grids

Permalink to "Testing Target Size 2.5.8 in Dense Grids"

Dense data grids are full of small targets: 16-pixel row checkboxes, icon-only row action buttons, tiny sort arrows in headers, column resize handles a few pixels wide, expand chevrons. WCAG 2.2 added SC 2.5.8 Target Size (Minimum) at level AA, requiring targets of at least 24×24 CSS pixels — or enough spacing around smaller ones. Users with tremor, limited dexterity or touch screens are most affected. Target size is measurable, so it belongs in automated tests.

This guide belongs to visual regression & theme testing.

Spec reference

Permalink to "Spec reference"

SC 2.5.8 Target Size (Minimum) (AA) — targets are at least 24 by 24 CSS pixels, except where:

  • Spacing: undersized targets are positioned so that a 24-pixel diameter circle centred on each target’s bounding box does not intersect another target or another target’s circle.
  • Equivalent: the function is available through another control on the same page that meets the criterion.
  • Inline: the target is in a sentence or its size is constrained by line height of non-target text.
  • User agent control: the size is determined by the browser and not modified by the author.
  • Essential: a particular presentation is essential or legally required.

SC 2.5.5 Target Size (Enhanced) (AAA) raises the bar to 44×44.

Target sizes in a dense grid row Mock of a grid row with a 16 px checkbox, the record name, a 16 px sort-like icon button and a 20 px row action button; the undersized targets are flagged. Target sizes in a dense grid rowSelectInvoiceCustomerActions16 px boxINV-1042Lund & Co20 px ⋯16 px boxINV-1043Mesa Foods20 px ⋯Checkboxes 4 px apart vertically fail both size and spacing; the action buttons pass spacing only if no other targetsits within 24 px.
Three targets fail the 24 px size; spacing decides whether they pass anyway.

When to use — and when not to

Permalink to "When to use — and when not to"

Test target size on every grid and toolbar in the product, in every density. Dense layouts are where failures cluster.

Do not lean on the “equivalent” exception casually; it requires another control on the same page that performs the same function and meets the size. A row action available in a bulk toolbar is an equivalent only if the toolbar acts on that row in a comparable way. Do not count the 24-pixel minimum against the visible icon; it is the target (hit area) that matters.

The misapplication: row checkboxes rendered at 16 pixels with 4 pixels between rows, justified as “inline”. Grid checkboxes are not in a sentence; the inline exception does not apply.

Visible size versus target size Comparison: the visible icon can be small; the hit area must reach 24 pixels or be spaced; padding and pseudo-elements enlarge the target without changing the look. Visible size versus target size✗ Visible icon 16 px, hit area 16 pxFails 2.5.8 unless spacedMis-taps on neighbouring rowsNo layout change needed"Fixing" by enlarging the icon✓ Visible icon 16 px, hit area 24 pxPassesFewer mis-tapsPadding or ::before expands the areaEnlarge the target, not necessarily the icon

Annotated code example

Permalink to "Annotated code example"
// target-size.spec.js — measure targets and the spacing exception (SC 2.5.8)
test('grid targets meet 2.5.8', async ({ page }) => {
  await page.goto('/invoices?density=compact');
  const failures = await page.evaluate(() => {
    const sel = 'button, a[href], input, select, [role="checkbox"], [role="button"], [role="columnheader"][aria-sort] button, [role="separator"][tabindex]';
    const targets = [...document.querySelectorAll(`[role="grid"] ${sel}, table ${sel}`)]
      .filter(el => el.offsetParent !== null)
      .map(el => ({ el, r: el.getBoundingClientRect() }));
    const small = targets.filter(t => t.r.width < 24 || t.r.height < 24);
    const centre = (r) => ({ x: r.left + r.width / 2, y: r.top + r.height / 2 });
    return small.filter(t => {                       // spacing exception: 24 px circles must not intersect
      const c = centre(t.r);
      return targets.some(o => o !== t && (() => {
        const oc = centre(o.r), dx = c.x - oc.x, dy = c.y - oc.y, d = Math.hypot(dx, dy);
        const otherSmall = o.r.width < 24 || o.r.height < 24;
        return otherSmall ? d < 24 : d < 12 + Math.min(o.r.width, o.r.height) / 2;   // approximation
      })());
    }).map(t => `${t.el.getAttribute('aria-label') || t.el.textContent.trim() || t.el.tagName} ${Math.round(t.r.width)}×${Math.round(t.r.height)}`);
  });
  expect(failures).toEqual([]);
});
/* Enlarge hit areas without changing visuals */
/* Wrong: margin: 4px around a 16 px checkbox does NOT enlarge its target */
.grid input[type="checkbox"] { inline-size: 16px; block-size: 16px; }
/* Right: a label wrapping the checkbox makes the whole 24 px box clickable */
.grid label.cb-wrap { display: inline-grid; place-items: center; min-inline-size: 24px; min-block-size: 24px; cursor: pointer; }
.icon-btn { min-inline-size: 24px; min-block-size: 24px; padding: 4px; }
.resize-handle::before { content: ""; position: absolute; inset: 0 -10px; }         /* wider hit area */

The comment in the first CSS rule is deliberate: margins do not increase a target’s hit area. Wrap the checkbox in a label (or the cell) so the clickable area grows.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"

Target size is about pointer input; keyboard and screen reader users are unaffected directly. But fixes can affect them:

Fix Keyboard / AT consideration
Label wrapping the checkbox Accessible name may change — keep it “Select INV-1042”
Larger icon buttons No change; ensure focus ring fits inside the cell
Wider resize handles Handles must remain keyboard operable (window splitter or column resize pattern)
Bigger touch areas via pseudo-elements No change
Exceptions and when they apply in grids Matrix of grid targets — row checkbox, row action icon, sort button in header, column resize handle, inline link in a cell — against the spacing, equivalent and inline exceptions. Exceptions and when they apply in gridsSpacingEquivalentInlineRow checkboxRarely, rows are closeIf bulk select existsNoRow action iconIf isolatedIf a menu offers the sameNoHeader sort buttonSometimesNoNoResize handleAdjacent to headerIf keyboard resize andcolumn menu existNoLink inside cell textNoNoIf inline in text
Most grid targets must meet the size or the spacing; few qualify for other exceptions.

Integration context

Permalink to "Integration context"

Dense grids interact with density settings — see segmented controls for view density and density tokens and target size in data grids. Mobile tables have stricter practical needs, covered in touch targets in mobile data tables. Resize handles also need keyboard support per keyboard accessible column resizing.

Gotchas

Permalink to "Gotchas"

Bounding boxes versus hit areas. getBoundingClientRect measures the element, not pseudo-element hit areas. If you enlarge targets with ::before, measure the larger area in tests (or use elementFromPoint sampling).

Zoom. Target size is in CSS pixels, so browser zoom does not help or hurt the measurement — test at 100%.

Spacing calculations. The exact spacing rule uses circles centred on bounding boxes; approximations in tests can over- or under-report. Treat flagged items as candidates for review.

Common mistakes

Permalink to "Common mistakes"
  • 16-pixel row checkboxes with no wrapper.
  • Using margins to “enlarge” targets.
  • Claiming the inline exception for grid controls.
  • Testing only the comfortable density.

Framework notes

Permalink to "Framework notes"

MUI Checkbox has padding that already makes a 38–42 px target by default; compact grids often remove it — check the size="small" variant. AG Grid checkbox selection renders small checkboxes; wrap them via cell renderers or increase rowHeight. Angular Material density scales below -2 reduce touch targets; test each density you allow.

// MUI X: keep a 24 px minimum target in compact density
<DataGrid density="compact" slotProps={{ baseCheckbox: { sx: { p: '4px' } } }} />

Worked example

Permalink to "Worked example"

A trading blotter grid at compact density had 14-pixel checkboxes, 18-pixel row action icons and 4-pixel resize handles. The automated test flagged 312 targets per page. Wrapping checkboxes in 24-pixel label cells, padding icon buttons, widening resize hit areas with pseudo-elements, and adding a column menu with “Resize column…” as an equivalent brought the count to zero, without changing the compact look. Mis-click complaints from traders on touch laptops fell sharply.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Define minimum target tokens (24 px at AA; 44 px recommended for touch) and build them into checkbox, icon-button and handle components, so density changes padding around content but never shrinks hit areas below the minimum.

FAQ

Permalink to "FAQ"
Is 24×24 the visible size or the clickable area?

The target — the area that responds to pointer input. Visuals can be smaller if the hit area is large enough.

Do row checkboxes need to be 24 px if rows are 32 px tall?

The checkbox target needs 24×24 or adequate spacing. A label or cell wrapper that is clickable is the easiest way.

Does 2.5.8 apply to mobile only?

No. It applies to all pointer input, including mouse.

What about 44 px?

That is AAA (2.5.5) and a good target for touch-first products.

Can I rely on the equivalent exception for resize handles?

If columns can also be resized by keyboard and via a menu that meets size requirements, yes.

How do I test hit areas enlarged with pseudo-elements?

Sample points around the element with document.elementFromPoint and check which element responds.

Permalink to "Related"

← Back to Visual Regression & Theme Testing