How this was measured
On 2026-10-17 we fetched the homepage of each of the same 78 hand-picked domains our other surveys use — news, commerce, SaaS, developer tooling, social, streaming, reference, SEO, education, finance, cloud, local and government — and stored the raw HTML. This article re-parses that stored HTML offline; no network request was made to produce any number below.
62 of the 78 returned a readable document and 60 of those are homepages. Sixteen never did: six answered 403 (ebay.com, stackoverflow.com, npmjs.com, searchengineland.com, yelp.com, nih.gov), six failed to connect at all (nytimes.com, reuters.com, washingtonpost.com, etsy.com, quora.com, coinbase.com), two answered 202 (amazon.com, booking.com), one answered 406 (uber.com) and archive.org answered 200 with an error page. Two more — khanacademy.org and paypal.com — returned a bot-challenge page and were discarded rather than parsed. Of the 62 usable responses, 42 came over a direct connection and 20 through a proxy.
Then the part that decided every number in this article: deciding which controls to judge at all. We stripped comments, script, style, template and noscript content first, so a tag inside a JavaScript string or a commented-out block is never counted. We collected every <input>, <select> and <textarea>, dropped type="hidden" inputs (94 of them) and every button-like input, which left 146 controls. Then we removed four kinds of control that a screen reader never reaches:
- 13 hidden by their own attributes —
hidden,aria-hidden="true", an inlinedisplay:none, or the standard visually-hidden CSS (clip-path:inset(50%),clip:rect(0,0,0,0), 1×1 withoverflow:hidden). - 1 whose class name suggests
display:none(Tailwind.hidden, Bootstrap.d-none). A static parser cannot read external CSS, so this one is filed as unresolvable rather than as a defect. - 58 that are perfectly clean but sit inside a hidden ancestor: collapsed modals, hidden mobile menus, SvelteKit's
<div hidden>hydration templates.
That left 74 controls on 33 of the 60 homepages as the honest denominator. We had to build the ancestor layer mid-survey: without it the same code reported 133 controls and 31 of them apparently relying on a placeholder for their name, because forbes.com ships 19 identical copies of its search input inside a <nav style="display:none">. Every exclusion is printable with --evidence=anc, which names the exact ancestor element that triggered it.
For each surviving control we computed an accessible name the way the specification says a browser should, in priority order: aria-labelledby, then aria-label, then an associated <label> (either for= or wrapping), then title, then placeholder as the last resort. An aria-labelledby pointing at an id that does not exist does not count as a name. One parser bug is worth recording because it changed a published-adjacent number: our first version matched only attributes written as name=value, so <input hidden> — a boolean attribute with no value — read as "visible". youtube.com's hidden search input was being reported as a nameless control until we fixed it.
Finding 1: more than half of these homepages have no form at all
27 of the 60 homepages contain a <form> element (45.0%), 61 forms between them, a median of one each and a maximum of 19 — forbes.com, all 19 of which live inside that same hidden navigation block. 33 sites expose at least one control a screen reader reaches, so at least six sites ship inputs that are not inside any form (a JS-driven search box wired to a handler, typically).
Of the 61 forms: 34 declare an action (55.7%) and 27 do not, which is legal — an omitted action submits back to the current URL — and usually means the form is handled in JavaScript. Only 3 use method="post"; the other 58 either say get or say nothing, and only 21 write the attribute explicitly at all. 16 forms carry role="search", and not one carries any other role. No form in the sample is empty, and 4 declare novalidate to switch off native constraint validation.
The shape of the sample is dominated by one use case: a homepage form is almost always a search box or an email capture. That is why 94 hidden inputs outnumber the 74 controls users can actually reach — CSRF tokens, tracking parameters and framework state travel in every form, invisible to everyone.
Finding 2: where the 74 names come from
45 of the 74 controls (60.8%) get their name from a real <label> — 31 through for= and 14 through a wrapping label. 21 (28.4%) use aria-label, 4 (5.4%) fall back to placeholder, 2 (2.7%) fall back to title and 2 (2.7%) have no name at all.
The pooled percentage is not the useful number here, because one site with twelve controls drowns out twenty sites with one. The median per-site share of controls named by a real label is 33.3%, and the more legible measure is how many sites are affected at least once: 17 of the 33 sites use a real label somewhere, 4 rely on a placeholder somewhere, and 2 ship a control with no name anywhere. A third of the sites with a form never write a <label> at all, and they get away with it because aria-label is a valid substitute.
Finding 3: the two controls with no name, read back from the source
Both are <select> elements, and both were printed and read before being written down here.
wikipedia.org: <select id="searchLanguage" name="language">, sitting inside <div class="styled-select no-js"> in the portal's search form, next to an input that does have a label (<label for="searchInput" class="screen-reader-text">Search Wikipedia</label>). The no-js class suggests this is the fallback language picker for browsers without JavaScript. We cannot confirm from HTML whether it is displayed when JS is on — but if it is, it is a language dropdown announced with no name.
moz.com: <select class="form-control selectpicker d-block d-md-none mb-5" id="mobileSelect-tabs-304"> inside <nav class="tabs-block">. Bootstrap's d-md-none means hidden from 768px up, so this is the mobile tab switcher for the section below it — the one that chooses between "AI Visibility", "SEO" and "SEO at Scale". On a phone, in a screen reader, it announces as an unlabelled combo box.
Two out of 74 is a small number, and we are not going to inflate it. The reason it is worth printing is the pattern: both are <select> elements, both are the mobile or fallback version of a control that has a desktop equivalent, and both were presumably added later than the thing they duplicate. The controls people forget are the second copy.
Finding 4: four controls are named only by their placeholder
A placeholder is not a name. It disappears the moment the field has a value, it is not announced consistently across screen readers, it is low-contrast by default, and it leaves nothing for a voice-control user to say. Four controls in this sample have nothing else:
asana.com—<input id="signup-textbox" autofocus type="text" placeholder="[email protected]" name="email">. This is a signup email field on a homepage. The example address is the only thing labelling it.moz.com—<input type="text" class="mzr-input-search" name="q" placeholder="Search...">.screamingfrog.co.uk—<input value="" placeholder="Looking for something?" class="sf-searchbar__field" type="text" name="s" required>. It is alsorequired: the browser will refuse to submit and then announce a validation message about a field it cannot name.target.com—<input id="searchMobile" placeholder="What can we help you find?" tabindex="-1" ...>, the mobile duplicate of a desktop search input that does carry anaria-label.
Finding 5: buttons are the real problem, and the usual advice about them is noise
1,150 buttons across the 60 homepages, a median of 12 each and a maximum of 88 on vimeo.com. After removing the ones inside hidden containers, 877 are auditable and 38 of those (4.3%) have no accessible name, spread over 11 sites. The pattern is always the same: an icon-only button with an <i> or an SVG inside and no text. theguardian.com alone contributes five video play/pause buttons differing only by a UUID in data-link-name; kubernetes.io ships <button id=hamburger class="btn fas fa-bars">; zoom.us ships carousel <button class="prev"> and <button class="next">.
One specific failure deserves naming because it looks correct in a code review: zoom.us ships <button class="fdn-logo-slide__control fdn-logo-slide__control--pause" aria-label=""> — an aria-label attribute containing an empty string. An empty aria-label does not provide a name; the accessible-name algorithm skips it and falls through. The attribute is there, the linter that greps for aria-label is satisfied, and the button is still anonymous.
The counter-example is the majority and it is done right: 286 buttons have no text content but do carry a non-empty aria-label, and 15 more use aria-labelledby. Labeling an icon button is a solved problem that most of this sample has solved.
Now the myth. 602 <button> elements across 39 sites have no type attribute, which trips the widely repeated rule "always write type or the button will submit the form". Only 6 of those 602 are actually inside a <form> — on cnn.com, walmart.com, asana.com, moz.com, screamingfrog.co.uk and heroku.com. The other 596 are accordions, tabs, hamburger menus, carousels and play buttons sitting outside any form, where the default submit type does nothing at all. The advice is correct and the scope in which it matters is 1% of the cases where it is applied.
Finding 6: 21 sites have a search box and only 11 say so
29 inputs look like a search field — type="search", or a name of q, s, search, query or searchterm — across 21 sites. 15 use type="search" and 13 use type="text", which gives up the clear button, the mobile keyboard's search key and the platform styling for free. One more, mit.edu, writes type="query", which is not an input type at all: an unrecognised type falls back to text, so the attribute is silently doing nothing.
Only 11 of those 21 sites wrap the field in a form with role="search" (16 forms in total, because some sites have a desktop and a mobile copy). That role is what turns a form into a landmark a screen reader user can jump to directly; without it the search box is one more thing to walk past. Naming is better than structure here: 25 of the 29 have a real accessible name, mostly aria-label="Search", and 4 fall back to the placeholder.
Finding 7: autocomplete is almost only ever used to switch autofill off
25 of the 74 controls declare autocomplete (33.8%), and 15 of those 25 set it to off. Read the list of what is left and the story tells itself: email five times, on once, and username, given-name, family-name and tel once each. Ten controls in the entire sample ask a browser to fill in something useful.
That is not automatically wrong. Most of the off values are on search boxes, where autofill is noise and autocomplete="off" is the correct call — and on wise.com's currency amount inputs, where a remembered value would be actively misleading. But the asymmetry is the finding: autocomplete is treated as a switch for turning autofill off, not as the mechanism (and WCAG 1.3.5 success criterion) for letting people avoid re-typing their own details. 11 controls take an email address or have "email" in their name; 6 of them say so to the browser.
Zero password fields appear in this sample, which is not a claim about the web — it is a claim about homepages. The login forms that would carry current-password and new-password live one click away, and this survey never clicks.
Finding 8: the label wiring itself is clean
Two things that audit tools love to flag turned up zero times, and we checked the parser before believing it. No <label for="…"> in the sample points at an id that does not exist, and no aria-labelledby points at a missing id either. Where sites name their controls, they wire them up correctly.
What does go wrong is id uniqueness, which matters here because label for= and aria-labelledby both resolve to the first match. 17 sites ship duplicate ids, 70 of them, after excluding 43 duplications of clipPath and linearGradient ids inside inline SVG — those are the same <symbol> referenced twice and are not defects. The extremes are slack.com with promo_aria_description sixteen times and atlassian.com with jira seven times, collectionteamwork six, confluence five, collectionsoftware and collectionproduct four each. None of these ids is currently targeted by a label, so nothing is broken today; they are a trap for the next person who adds one.
47 of the 74 controls (63.5%) have an id at all. The other 27 cannot be targeted by label for=, which is a fair part of why wrapping labels and aria-label are as common as they are.
Finding 9: the "aria-hidden on a focusable control" finding that isn't one
12 controls carry aria-hidden="true" across 5 sites, which looks like the worst defect in this article: an element hidden from assistive technology but still reachable by keyboard. None of them are.
9 are the CSS-only menu pattern done correctly — hidden checkboxes driving a :checked selector on theguardian.com, asana.com, vimeo.com and cloudflare.com, each also carrying tabindex="-1" so it is out of the focus order. Hiding them from the accessibility tree is the right call; they are CSS machinery, not controls. 3 are on shopify.com with class="peer hidden", and the page's own stylesheet defines .hidden{display:none}, so they are almost certainly fine too — but a static parser cannot resolve a class name against a stylesheet, so we file them as unresolvable rather than innocent or guilty.
The lesson generalises past this sample: a rule that counts aria-hidden on a form control without checking whether the element is focusable will report nine false positives in five sites. We reported the same split in our image survey, where a naive alt check accused bbc.com of 71 missing attributes that were in fact React placeholder images with aria-label.
Finding 10: radio and checkbox groups are almost never grouped
The sample contains 26 checkboxes and 18 radio buttons, and 10 <fieldset> elements against 2 <legend>. Seven sites use a fieldset at all — squareup.com (3), screamingfrog.co.uk (2), then asana.com, python.org, wikipedia.org, coursera.org and vercel.com — and five of those seven have more fieldsets than legends. A fieldset with no legend groups its controls with no name for the group, which is roughly half as useful as not grouping them.
In fairness, most of the checkboxes and radios here are not form groups at all — they are the hidden CSS-menu checkboxes above, cookie toggles and filter widgets. Grouping only matters where several controls answer one question, and homepage forms rarely ask one.
What to ship
Give every control a name that is not its placeholder. Two controls here have no name and four are named by a placeholder. A <label> is best because it is visible and clickable; aria-label is a valid substitute and it is what 28% of this sample uses.
Check the mobile copy, not the desktop copy. Both nameless controls and two of the four placeholder-only controls are the second version of a control that has a labelled desktop equivalent. d-md-none and d-none d-md-block are where labels go missing.
Name your icon buttons, and never write an empty aria-label. 38 buttons in this sample are anonymous and one has aria-label="", which reads as present to a grep and as nothing to a screen reader.
Write type on buttons that are inside a form — and only those. 602 buttons here omit it and 6 of them matter.
Use type="search" and role="search". 13 of 29 search inputs are plain text and 10 of 21 sites never mark the landmark. Both are one attribute.
Use autocomplete to fill things in, not only to stop it. Ten controls in 60 homepages ask for useful autofill; 11 email fields exist and 6 declare themselves.
Do not trust a duplicate id you did not just create. 17 sites have them. label for= resolves to the first one.
To see how the rest of the page holds up, our document metadata survey measures the lang, charset and viewport tags that surround these forms, and our image survey covers the other half of homepage accessibility. The heading analyzer reads a page's outline from pasted HTML the same way this survey read its controls — statically, from the source, with no rendering.
Limitations
The biggest one: this is static HTML, and a large share of forms are not in it. React, Vue and Svelte clients render inputs after the document loads; a search box that only exists after hydration is invisible to this method and so is its label. The 94 hidden inputs and the SvelteKit <div hidden> templates are evidence of how much machinery sits behind these pages. Every number here is a floor on the controls that exist and a ceiling on nothing.
Homepages only, and they are the easiest page. Login, checkout, settings and contact forms are where labels matter most and they are all one click away from this sample. type="password" appears zero times, which is a fact about homepages and not about the web.
We never ran a screen reader. Every verdict is our implementation of the accessible-name computation order applied to parsed HTML. Where the specification is subtle — empty aria-label, aria-labelledby resolution — we followed the written algorithm, not observed behaviour in any particular browser.
Visibility is inferred, not measured. We excluded 72 controls using four heuristics: explicit attributes, inline styles, class names and ancestor elements. Real CSS is not available to us, so a control hidden only by an external rule is counted as visible, and a control inside a container we judged hidden might in fact be shown by JS. Both errors are possible; the --evidence=anc output exists so that every one of the 72 can be checked by hand.
One snapshot, fetched on 2026-10-17, from an exit IP in Asia, over two different network paths (42 direct, 20 proxied). Regional variants serve different markup; uber.com served a German edition to our other surveys. And 16 of 78 domains never answered, several of them WAFs blocking our network rather than site failures — which biases the sample towards sites that are permissive of crawlers.
Reproduce it
The script and the shared domain list ship with this site. node scripts/survey-resource-hints.mjs fetches the homepages and writes .workbuddy/tmp/resource-hints-survey/raw.json; node scripts/survey-forms.mjs --raw=.workbuddy/tmp/resource-hints-survey/raw.json re-derives every number in this article from that file without touching the network. Pinning the snapshot matters: re-running without --raw picks the newest stored snapshot, and numbers from a different day will not match these. --selftest asserts the parser against a synthetic page first — including boolean hidden, an ancestor with display:none, and a class="hidden md-block" that must not be read as a boolean attribute. --evidence prints the original tag behind every verdict; --evidence=noname, =placeholder, =btnnone, =search, =acoff and =anc narrow it to one class of finding, and every line carries its domain.