Give it a try
last updated: 08/04/2026
-
To see your bookmarks bar, press the keys
(Ctrl/Cmd + Shift + B). - Drag the "SpotCheck - Input Label Checker" link to your bookmarks bar or bookmark folder.
- Open any page, then click the bookmark to run it.
- 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.
●2 explicit
●2 implicit
●1 no label
What the colors mean
Each control gets an outline and a badge. The color says how the control is named, not just whether it is named.
The control is named by a
label
whose
for
attribute matches the control's
id. This is the most durable
association, and it survives markup being moved around.
The control is wrapped in a
label
with no
for
attribute, or it is named only by
aria-label,
aria-labelledby, or
title. It works today, but it
is easier to break.
Nothing gives the control an accessible name. A
placeholder
does not count; it disappears as soon as someone starts typing.
Try it here
Run the check against the sample markup below.
Applicable WCAG 2.2 success criteria
Labeling touches several criteria at once. These are the ones this check speaks to most directly.
-
1.3.1
Info and Relationships (Level A)
The relationship between a label and its control has to be exposed in the markup, not just implied by visual proximity.
-
2.4.6
Headings and Labels (Level AA)
Once a label exists, it still has to describe what the field is asking for.
-
2.5.3
Label in Name (Level A)
When a visible label is present, the accessible name has to contain that same text so speech input users can target the field.
-
3.3.2
Labels or Instructions (Level A)
Every control that needs user input needs a label or instructions telling people what to enter.
-
4.1.2
Name, Role, Value (Level A)
A form control with no accessible name is announced by its role alone, which tells a screen reader user nothing about the field.
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 = "__a11yInputLabelChecker";
if (w[STATE_KEY]) {
w[STATE_KEY].clear();
return;
}
var COLORS = {
explicit: "#1a7d4f",
implicit: "#8b6800",
missing: "#be412a"
};
var OUTLINES = {
explicit: "5px solid",
implicit: "6px dotted",
missing: "5px dashed"
};
var OUTLINE_OFFSET = "3px";
var DOTS = {
explicit: "#3fbf7f",
implicit: "#e0b84f",
missing: "#e06a4f"
};
var BADGES = {
explicit: "Explicit label",
implicit: "Implicit label",
missing: "No label"
};
var SKIP_TYPES = ["hidden", "submit", "reset", "button", "image"];
var records = [];
var counts = { explicit: 0, implicit: 0, missing: 0 };
var panel = null;
function normalize(value) {
return (value || "").replace(/\s+/g, " ").trim();
}
function textFrom(el) {
return el ? normalize(el.textContent) : "";
}
function labelsFor(el) {
if (el.labels && el.labels.length) {
return Array.prototype.slice.call(el.labels);
}
var found = [];
if (el.id) {
Array.prototype.slice.call(d.querySelectorAll("label")).forEach(function (label) {
if (label.getAttribute("for") === el.id) {
found.push(label);
}
});
}
var wrapping = el.closest ? el.closest("label") : null;
if (wrapping && found.indexOf(wrapping) === -1) {
found.push(wrapping);
}
return found;
}
function ariaLabelledByText(el) {
var ids = normalize(el.getAttribute("aria-labelledby"));
if (!ids) {
return "";
}
return normalize(
ids
.split(" ")
.map(function (id) {
return textFrom(d.getElementById(id));
})
.join(" ")
);
}
function classify(el) {
var hasExplicit = false;
var hasImplicit = false;
labelsFor(el).forEach(function (label) {
if (!textFrom(label)) {
return;
}
if (el.id && label.getAttribute("for") === el.id) {
hasExplicit = true;
} else if (label.contains(el)) {
hasImplicit = true;
}
});
if (hasExplicit) {
return "explicit";
}
if (hasImplicit) {
return "implicit";
}
if (ariaLabelledByText(el)) {
return "implicit";
}
if (normalize(el.getAttribute("aria-label"))) {
return "implicit";
}
if (normalize(el.getAttribute("title"))) {
return "implicit";
}
return "missing";
}
function makeBadge(kind) {
var badge = d.createElement("span");
badge.setAttribute("data-a11y-label-check", "badge");
badge.textContent = BADGES[kind];
badge.style.cssText = [
"display:inline-block",
"width:fit-content",
"justify-self:start",
"margin:0 0 0 4px",
"padding:4px 8px",
"border-radius:4px",
"background:" + COLORS[kind],
"color:#fff",
"font:500 16px/1.2 Arial, Helvetica, \"Helvetica Neue\", sans-serif",
"letter-spacing:0.04em",
"vertical-align:middle",
"position:relative",
"z-index:2147483646"
].join(";");
return badge;
}
function mark(el) {
var kind = classify(el);
var record = {
el: el,
outline: el.style.outline,
offset: el.style.outlineOffset,
badge: makeBadge(kind)
};
counts[kind] += 1;
el.style.outline = OUTLINES[kind] + " " + COLORS[kind];
el.style.outlineOffset = OUTLINE_OFFSET;
el.insertAdjacentElement("afterend", record.badge);
records.push(record);
}
function makeCountLine(kind, 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[kind] + ";margin-right:6px";
line.appendChild(dot);
line.appendChild(d.createTextNode(counts[kind] + " " + word));
return line;
}
function makePanel() {
var wrapper = d.createElement("div");
var heading = d.createElement("strong");
var close = d.createElement("button");
wrapper.setAttribute("data-a11y-label-check", "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 = "Input Label Checker";
heading.style.cssText = "display:block;margin-bottom:4px";
wrapper.appendChild(heading);
wrapper.appendChild(makeCountLine("explicit", "explicit"));
wrapper.appendChild(makeCountLine("implicit", "implicit"));
wrapper.appendChild(makeCountLine("missing", "no label"));
close.type = "button";
close.textContent = "\u00d7";
close.setAttribute("aria-label", "Clear the label 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];
}
Array.prototype.slice
.call(d.querySelectorAll("input, select, textarea"))
.filter(function (el) {
return SKIP_TYPES.indexOf(el.type) === -1;
})
.forEach(mark);
panel = makePanel();
d.body.appendChild(panel);
w[STATE_KEY] = { clear: clear };
})();
FYI
-
Explicit and implicit labels both work, but explicit labels are
more robust. Explicit is marked green because a
forandidpair keeps working when the markup gets restructured, while a wrapping label breaks the moment the control is moved out of it. -
aria-label,aria-labelledby, andtitleare counted as gold. They give the control a name, but there typically isn't a visible label tied to it, so sighted speech input users have nothing to say out loud.titleis the weakest of the three and should be a last resort. -
Buttons are skipped.
buttonelements and inputs of type submit, reset, button, and image take their name from their content,value, oraltinstead of a label. -
Hidden inputs are skipped, and a
labelwith no text counts as no label at all. -
Grouped controls additional verification. Radio and checkbox
groups also need a
fieldsetand alegend, which this tool does not cover. -
Custom widgets are also out of scope. Anything built from
divelements withrole="textbox"orcontenteditableis not evaluated.