Give it a try

SpotCheck - Target Size Checker

last updated: 08/21/2026

  1. To see your bookmarks bar, press the keys (Ctrl/Cmd + Shift + B) .
  2. Drag the "SpotCheck - Target Size Checker" link to your bookmarks bar or bookmark folder.
  3. Open any page, then click the bookmark to run it.
  4. Click it again or click the close button in the popup to clear the results.

Example of the popup that will appear in the bottom right corner of your browser with a summary of the results.

Target Size Checker 12 meet 44 by 44   5 meet 24 by 24   3 under 24 by 24   9 inline, exempt

What the colors mean

Each target gets a colored outline and a badge with its measured width and height.

Enhanced

The target is at least 44 by 44 CSS pixels, so it satisfies both 2.5.8 Minimum and 2.5.5 Enhanced.

Minimum

The target is at least 24 by 24, or it is smaller but has enough clear space around it to use the spacing exception. It passes AA but not AAA.

Too small

The target is under 24 by 24 and sits close enough to another target that the spacing exception does not apply.

Exempt

The target sits inline in a sentence, where its size is set by the surrounding line height. Both criteria exempt it.

Try it here

Run the check against the sample controls below. Several of them are deliberately undersized so there is something to fail. Each control gets an outline and a badge with its measurement, the same way the bookmarklet marks up a real page.

Skim the sizing notes before you shrink anything, because inline links in running text are measured differently.

Applicable WCAG 2.2 success criteria

Small targets are hard to hit for anyone with a tremor, limited dexterity, or a touchscreen and a thumb, so both criteria set a floor on how small a control is allowed to be.

  • 2.5.8
    Target Size (Minimum)(Level AA)

    Pointer targets are at least 24 by 24 CSS pixels, unless the spacing, equivalent, inline, user agent, or essential exception applies.

  • 2.5.5
    Target Size (Enhanced)(Level AAA)

    Pointer targets are at least 44 by 44 CSS pixels, unless the equivalent, inline, user agent, or essential exception applies. There is no spacing exception at this level.

Read the source

The bookmarklet contains the TL;DR version of the code. Here's the readable version.

(function () {
  var w = window;
  var d = document;
  var STATE_KEY = "__a11yTargetSizeChecker";

  if (w[STATE_KEY]) {
    w[STATE_KEY].clear();
    return;
  }

  var MIN_SIZE = 24;
  var ENHANCED_SIZE = 44;
  var SPACING_RADIUS = 12;
  var LABEL_GAP = 24;

  var COLORS = {
    green: "#1a7d4f",
    gold: "#8b6800",
    red: "#be412a",
    grey: "#4a5464"
  };

  var OUTLINES = {
    green: "5px solid",
    gold: "6px dotted",
    red: "6px dashed",
    grey: "3px dotted"
  };

  var OUTLINE_OFFSET = "3px";

  var DOTS = {
    green: "#3fbf7f",
    gold: "#e0b84f",
    red: "#e06a4f",
    grey: "#9aa4b2"
  };

  var LEVELS = {
    enhanced: "green",
    minimum: "gold",
    spacing: "gold",
    small: "red",
    inline: "grey"
  };

  var BADGES = {
    enhanced: "Meets 2.5.5 Enhanced",
    minimum: "Meets 2.5.8 Minimum",
    spacing: "Under 24, spacing exception",
    small: "Under 24 by 24",
    inline: "Inline in text, exempt"
  };

  var SELECTOR = [
    "a[href]",
    "area[href]",
    "button",
    "input:not([type=hidden])",
    "select",
    "textarea",
    "summary",
    "audio[controls]",
    "video[controls]",
    "[tabindex]:not([tabindex^='-'])",
    "[role=button]",
    "[role=link]",
    "[role=checkbox]",
    "[role=radio]",
    "[role=switch]",
    "[role=tab]",
    "[role=menuitem]",
    "[role=menuitemcheckbox]",
    "[role=menuitemradio]",
    "[role=option]",
    "[role=slider]",
    "[role=spinbutton]",
    "[role=treeitem]"
  ].join(",");

  var records = [];
  var counts = { green: 0, gold: 0, red: 0, grey: 0 };
  var panel = null;

  function normalize(value) {
    return (value || "").replace(/\s+/g, " ").trim();
  }

  function isOperable(el) {
    if (el.disabled) {
      return false;
    }
    if (el.getAttribute("aria-disabled") === "true") {
      return false;
    }
    if (el.closest("[inert]")) {
      return false;
    }
    var style = getComputedStyle(el);
    if (style.display === "none" || style.visibility === "hidden") {
      return false;
    }
    var rect = el.getBoundingClientRect();
    return rect.width > 0 && rect.height > 0;
  }

  function isNested(el, lookup) {
    var parent = el.parentElement;
    while (parent) {
      if (lookup.has(parent)) {
        return true;
      }
      parent = parent.parentElement;
    }
    return false;
  }

  function union(a, b) {
    var left = Math.min(a.left, b.left);
    var top = Math.min(a.top, b.top);
    var right = Math.max(a.right, b.right);
    var bottom = Math.max(a.bottom, b.bottom);
    return {
      left: left,
      top: top,
      right: right,
      bottom: bottom,
      width: right - left,
      height: bottom - top
    };
  }

  function isAdjacent(a, b) {
    var gapX = Math.max(a.left - b.right, b.left - a.right);
    var gapY = Math.max(a.top - b.bottom, b.top - a.bottom);
    return gapX < LABEL_GAP && gapY < LABEL_GAP;
  }

  function labelRect(el) {
    var type = (el.getAttribute("type") || "").toLowerCase();
    if (el.tagName !== "INPUT" || (type !== "checkbox" && type !== "radio")) {
      return null;
    }
    var label = el.labels && el.labels.length ? el.labels[0] : null;
    if (!label) {
      return null;
    }
    var rect = label.getBoundingClientRect();
    if (!rect.width || !rect.height) {
      return null;
    }
    return rect;
  }

  function targetRect(el) {
    var rect = el.getBoundingClientRect();
    var label = labelRect(el);
    if (label && isAdjacent(rect, label)) {
      return union(rect, label);
    }
    return rect;
  }

  function isInlineTarget(el) {
    if (getComputedStyle(el).display !== "inline") {
      return false;
    }
    var parent = el.parentNode;
    if (!parent) {
      return false;
    }
    var nodes = parent.childNodes;
    var text = "";
    var i;
    for (i = 0; i < nodes.length; i += 1) {
      if (nodes[i] !== el && nodes[i].nodeType === 3) {
        text += nodes[i].nodeValue;
      }
    }
    return normalize(text).length > 0;
  }

  function center(rect) {
    return {
      x: rect.left + rect.width / 2,
      y: rect.top + rect.height / 2
    };
  }

  function circleHitsRect(point, radius, rect) {
    var nearestX = Math.max(rect.left, Math.min(point.x, rect.right));
    var nearestY = Math.max(rect.top, Math.min(point.y, rect.bottom));
    var dx = point.x - nearestX;
    var dy = point.y - nearestY;
    return dx * dx + dy * dy < radius * radius;
  }

  function passesSpacing(item, items) {
    var point = center(item.rect);
    var i;
    var other;
    var otherPoint;
    var dx;
    var dy;

    for (i = 0; i < items.length; i += 1) {
      other = items[i];
      if (other === item) {
        continue;
      }
      if (other.undersized) {
        otherPoint = center(other.rect);
        dx = point.x - otherPoint.x;
        dy = point.y - otherPoint.y;
        if (dx * dx + dy * dy < MIN_SIZE * MIN_SIZE) {
          return false;
        }
      } else if (circleHitsRect(point, SPACING_RADIUS, other.rect)) {
        return false;
      }
    }
    return true;
  }

  function classify(item, items) {
    if (item.rect.width >= ENHANCED_SIZE && item.rect.height >= ENHANCED_SIZE) {
      return "enhanced";
    }
    if (isInlineTarget(item.el)) {
      return "inline";
    }
    if (item.rect.width >= MIN_SIZE && item.rect.height >= MIN_SIZE) {
      return "minimum";
    }
    return passesSpacing(item, items) ? "spacing" : "small";
  }

  function labelFor(item, kind) {
    var size =
      Math.round(item.rect.width) + "\u00d7" + Math.round(item.rect.height);
    return size + " \u00b7 " + BADGES[kind];
  }

  function makeBadge(item, kind) {
    var level = LEVELS[kind];
    var badge = d.createElement("div");

    badge.setAttribute("data-a11y-target-size", "badge");
    badge.setAttribute("aria-hidden", "true");
    badge.textContent = labelFor(item, kind);
    badge.style.cssText = [
      "position:absolute",
      "width:fit-content",
      "max-width:min(90vw, 24rem)",
      "padding:2px 6px",
      "border-radius:4px",
      "background:" + COLORS[level],
      "color:#fff",
      "font:500 13px/1.3 Arial, Helvetica, \"Helvetica Neue\", sans-serif",
      "pointer-events:none",
      "z-index:2147483646"
    ].join(";");
    return badge;
  }

  function placeBadge(item, badge) {
    var badgeRect = badge.getBoundingClientRect();

    badge.style.top = item.rect.top + w.scrollY - badgeRect.height - 4 + "px";
    badge.style.left = item.rect.left + w.scrollX + "px";
  }

  function mark(item, items) {
    var kind = classify(item, items);
    var level = LEVELS[kind];
    var el = item.el;
    var record = {
      el: el,
      outline: el.style.outline,
      offset: el.style.outlineOffset,
      badge: makeBadge(item, kind)
    };

    counts[level] += 1;
    el.style.outline = OUTLINES[level] + " " + COLORS[level];
    el.style.outlineOffset = OUTLINE_OFFSET;
    d.body.appendChild(record.badge);
    placeBadge(item, record.badge);
    records.push(record);
  }

  function makeCountLine(level, word) {
    var line = d.createElement("p");
    var dot = d.createElement("span");

    line.style.cssText = "margin:0;padding:0";
    dot.textContent = "\u25cf";
    dot.style.cssText = "color:" + DOTS[level] + ";margin-right:6px";
    line.appendChild(dot);
    line.appendChild(d.createTextNode(counts[level] + " " + word));
    return line;
  }

  function makePanel() {
    var wrapper = d.createElement("div");
    var heading = d.createElement("strong");
    var close = d.createElement("button");

    wrapper.setAttribute("data-a11y-target-size", "panel");
    wrapper.setAttribute("role", "status");
    wrapper.style.cssText = [
      "position:fixed",
      "right:16px",
      "bottom:16px",
      "max-width:260px",
      "padding:12px 32px 12px 16px",
      "border-radius:6px",
      "background:#1b2430",
      "color:#e7eaed",
      "font:400 14px/1.5 Arial, Helvetica, \"Helvetica Neue\", sans-serif",
      "box-shadow:0 8px 24px rgba(0,0,0,0.35)",
      "z-index:2147483647"
    ].join(";");

    heading.textContent = "Target Size Checker";
    heading.style.cssText = "display:block;margin-bottom:4px";
    wrapper.appendChild(heading);

    wrapper.appendChild(makeCountLine("green", "meet 44 by 44"));
    wrapper.appendChild(makeCountLine("gold", "meet 24 by 24"));
    wrapper.appendChild(makeCountLine("red", "under 24 by 24"));
    wrapper.appendChild(makeCountLine("grey", "inline, exempt"));

    close.type = "button";
    close.textContent = "\u00d7";
    close.setAttribute("aria-label", "Clear the target size check results");
    close.style.cssText = [
      "position:absolute",
      "top:6px",
      "right:6px",
      "width:24px",
      "height:24px",
      "padding:2px 6px",
      "border:none",
      "background:transparent",
      "color:#e7eaed",
      "font:16px/1 Arial, Helvetica, \"Helvetica Neue\", sans-serif",
      "cursor:pointer"
    ].join(";");
    close.addEventListener("click", clear);
    wrapper.appendChild(close);

    return wrapper;
  }

  function clear() {
    records.forEach(function (record) {
      record.el.style.outline = record.outline;
      record.el.style.outlineOffset = record.offset;
      if (!normalize(record.el.getAttribute("style"))) {
        record.el.removeAttribute("style");
      }
      if (record.badge.parentNode) {
        record.badge.parentNode.removeChild(record.badge);
      }
    });
    records = [];
    if (panel && panel.parentNode) {
      panel.parentNode.removeChild(panel);
    }
    panel = null;
    delete w[STATE_KEY];
  }

  var elements = Array.prototype.slice
    .call(d.querySelectorAll(SELECTOR))
    .filter(isOperable);

  var lookup = new WeakSet(elements);

  var items = elements
    .filter(function (el) {
      return !isNested(el, lookup);
    })
    .map(function (el) {
      var rect = targetRect(el);
      return {
        el: el,
        rect: rect,
        undersized: rect.width < MIN_SIZE || rect.height < MIN_SIZE
      };
    });

  items.forEach(function (item) {
    mark(item, items);
  });

  panel = makePanel();
  d.body.appendChild(panel);
  w[STATE_KEY] = { clear: clear };
})();

FYI

  • Sizes are read from the rendered bounding box, so padding counts toward the target but a transparent margin does not.
  • The spacing exception is calculated the way the criterion describes it: a 24 pixel circle is centered on each undersized target, and the target passes only if that circle clears every other target and every other undersized target's circle.
  • For checkboxes and radios, the associated <label> is folded into the measurement when it sits right next to the control, since clicking the label activates it.
  • The inline exemption is a rule of thumb. It applies when the target computes to display: inline and has real text beside it in the same parent.
  • The exceptions. The equivalent-control, user-agent-default, and essential exceptions still need a human. A 16 pixel control can be conforming if an equal-sized alternative exists elsewhere on the page, so treat red as "go look at this" rather than "this is a violation."
  • Only targets rendered at load are measured. Anything inside a closed menu, a collapsed disclosure, or a modal that has not opened yet needs a second run once it is on screen.