#HTML and forms
AddedThese questions check that you know what the browser gives you for free when you pick the right element: a role, a name, keyboard behaviour and form behaviour, none of which a styled div has. Answer each one out loud before you open it. The first paragraph of each answer is what you'd say in the interview; the walkthrough is the reasoning behind it. Every "predict" answer comes with a live result whose hidden test proved it in Chrome 155.
<div class="btn">Save</div>
<button>Save</button>
<a>Docs</a>
<a href="/docs">Docs</a>Each one has a click handler attached in JavaScript. What does each one expose to a screen reader, and which ones can you Tab to?
Show answer
Only the <button> and the <a href>. The div and the <a> without href both come out as role generic with no name and can't be focused, so a keyboard user can't reach them and a screen reader doesn't announce them as controls. The <button> is a focusable button named "Save" that also activates with Enter and Space; the <a href> is a focusable link named "Docs" that activates with Enter. The rule I'd state: a button does something on this page, a link goes somewhere, and an <a> with no href is neither.
.btn { display: inline-block; padding: 4px 12px; border-radius: 6px; background: #2563eb; color: #fff; }<div class="btn">Save</div>
<button>Save</button>
<a>Docs</a>
<a href="/docs">Docs</a>- The
divstyled as a button looks identical to a real one, but its role isgenericand its name is empty: assistive tech sees an anonymous box.focus()on it does nothing, so Tab skips it and Enter or Space can never trigger the click handler. - The
<button>is abuttonnamed "Save" (from its text), it's in the tab order, and the browser turns Enter and Space into aclickfor you. That's the free behaviour you'd otherwise rebuild withtabindex="0",role="button"and a keydown handler. <a>Docs</a>withouthrefis a placeholder link: rolegeneric, not focusable. ItstabIndexproperty still reads0, which is a trap if you check focusability that way; the test only got focus onto elements that are really focusable.<a href="/docs">is alinknamed "Docs" and focusable. A link also gets "open in new tab", copy and bookmark, which is why navigation should be a link and not a button withlocation.href.- If you changed the
divto<div role="button" tabindex="0">, the role and focus would be fixed, but Enter and Space still wouldn't click it: ARIA changes what's announced, never what happens.
Learn more: Semantic HTML and why it matters, Links and navigation and The first rule of ARIA.
<form action="/search" method="get">
<input name="q" value="css grid">
<input value="unnamed">
<input name="lang" value="en" disabled>
<input type="checkbox" name="exact" checked>
<input type="checkbox" name="safe">
<select name="sort">
<option>newest</option>
<option value="top" selected>Top rated</option>
</select>
<button name="go" value="1">Search</button>
</form>The user clicks Search without changing anything. What's the query string?
Show answer
?q=css+grid&exact=on&sort=top&go=1. The form data set contains every control that has a name, isn't disabled and, for checkboxes and radios, is checked, plus the button that submitted the form if it has a name. Values are URL-encoded, so the space becomes +, a checkbox without a value sends on, and a select sends its selected option's value.
<form action="/search" method="get">
<input name="q" value="css grid">
<input value="unnamed">
<input name="lang" value="en" disabled>
<input type="checkbox" name="exact" checked>
<input type="checkbox" name="safe">
<select name="sort">
<option>newest</option>
<option value="top" selected>Top rated</option>
</select>
<button name="go" value="1">Search</button>
</form>qhas a name and a value, so it's sent asq=css+grid: theapplication/x-www-form-urlencodedformat encodes a space as+.- The second input has no
name, so it's left out completely;langisdisabled, so it's left out too (areadonlyfield would have been sent). exactis checked and has novalueattribute, so it sends the defaulton.safeis unchecked, so the key doesn't appear at all, which is why servers treat a missing checkbox as "false".- The
selectsends the selected option'svalue,top, not its text "Top rated". The first option has novalue, so when it's selected it sends its text,newest, which the test confirmed. - The button's
name=valuepair is only included when that button submitted the form.new FormData(form, button)builds the same list the browser sends on that click, and the test confirmed the click submits with the button asevent.submitter.
Learn more: How a form submits and select, datalist and textarea.
<label for="email">Email</label> <input id="email" type="email">
<label>Phone <input id="phone" type="tel"></label>
<label>City</label> <input id="city">
<input id="search" placeholder="Search">What accessible name does each input get, and which labels can you click to focus the field?
Show answer
Email and Phone are labelled; City and Search aren't. for/id and wrapping are the two ways to associate a <label>, and both give the input its name and make the label text clickable. A <label> next to an input without for is just text, so City has no name at all. Search does get a name, "Search", because Chrome falls back to the placeholder as a last resort, but a placeholder isn't a label: it disappears as soon as you type and is usually too faint to read.
label, input { margin: 4px 8px 4px 0; }<label for="email">Email</label> <input id="email" type="email"><br>
<label>Phone <input id="phone" type="tel"></label><br>
<label>City</label> <input id="city"><br>
<input id="search" placeholder="Search"><label for="email">points at the input'sid, soemail.labelshas one entry, the name is "Email", and clicking the label text moves focus into the field.- The wrapping label needs no ids: the input inside it is its labelled control, so Phone gets the same two benefits.
- The City label has no
forand doesn't wrap anything, so the browser can't connect it:labels.lengthis0, the name is empty and clicking "City" does nothing. Visually it looks labelled, which is why this bug survives code review. - Search has no label at all. Its name "Search" comes from the placeholder, the last step of the name computation, but the hint vanishes when the user types and placeholder text usually fails contrast, so it's still a failure.
Learn more: Labels and accessible names for inputs and Accessible names and descriptions.
input:invalid { outline: 3px solid crimson; }
input:user-invalid { background: #fee2e2; }<form>
<input id="email" type="email" required>
<input id="site" type="url" value="example.com">
<button>Send</button>
</form>Which fields have the red outline and which have the pink background when the page loads? And after the user presses Send?
Show answer
On load both fields have the red outline and neither is pink. After Send, both are pink, the submit is cancelled and focus goes to the email field. :invalid matches whenever the value fails a constraint, including before the user has typed anything, which is why forms styled with it shout at people on arrival. :user-invalid waits until the user has changed the field and moved on, or has tried to submit, and that's the one to style errors with.
input { margin-right: 6px; }
input:invalid { outline: 3px solid crimson; }
input:user-invalid { background: #fee2e2; }<form>
<input id="email" type="email" required>
<input id="site" type="url" value="example.com">
<button>Send</button>
</form>- The email field is
requiredand empty (valueMissing), and the URL field holdsexample.com, which has no scheme, so it failstype="url"(typeMismatch). Both match:invalidfrom the first frame and get the crimson outline. - Neither matches
:user-invalidyet, because the user hasn't interacted with either, so neither background is pink. A value written in the markup doesn't count as interaction. - Pressing Send runs interactive validation (the test calls
requestSubmit(), which does the same). The browser finds invalid fields, cancels the submit (nosubmitevent fired), focuses the first invalid one and shows its message. - A submit attempt also flips every field's "user validity" flag, so both now match
:user-invalidand turn pink at once. That's the behaviour you want: quiet until the user acts, then every problem flagged together.
Learn more: Styling valid and invalid fields and Built-in validation.
.banner { display: flex; gap: 8px; padding: 8px; background: #fef3c7; }<div class="banner" hidden>Settings saved</div>Show answer
No, it's visible. The hidden attribute works through one rule in the browser's own stylesheet, [hidden] { display: none }, and author CSS beats the user-agent origin whatever the specificity. So any display you set on the element, here flex, overrides it. The fix most resets include is [hidden] { display: none !important; }, which keeps the attribute in charge.
.banner { display: flex; gap: 8px; padding: 8px; margin-bottom: 8px; background: #fef3c7; }
.with-reset [hidden] { display: none !important; }<div class="banner" id="plain" hidden>Settings saved</div>
<div class="with-reset"><div class="banner" id="reset" hidden>Settings saved</div></div>hiddenis an HTML attribute, but it has no magic of its own: Chrome's user-agent stylesheet maps it todisplay: none, a normal declaration in the lowest origin.- The cascade compares origin before specificity, so the author rule
.banner { display: flex }wins even though[hidden]would tie with a class. The test readsdisplay: flexand a banner more than 20px tall: it shows. - In the second box, the reset
[hidden] { display: none !important; }(scoped to a wrapper here so both cases fit in one demo) beats the normaldisplay: flex, so the box is gone:display: none, height0. - That's why
!importantis acceptable here: the attribute's whole job is to hide, so nothing should be able to override it by accident.
Learn more: Global attributes and !important.
<form>
<input name="q">
<button>Clear</button>
<button type="submit">Search</button>
</form>The Clear button has a click handler that empties the field. The user types and presses Enter. What happens?
Show answer
The browser presses Clear. A <button> without a type is a submit button, and pressing Enter in a text field triggers the form's default button, the first submit button in tree order, by firing a click on it. So the field is emptied by Clear's handler and the form is submitted with Clear as the submitter. The fix is type="button" on every button that isn't meant to submit.
<form>
<input name="q">
<button id="clear">Clear</button>
<button type="submit" id="search">Search</button>
</form>$("#clear").typeis"submit": the missing attribute defaults to submit inside a form. So the form has two submit buttons, and the first in tree order is its default button.- The
:defaultpseudo-class matches exactly that button, and the test found it on Clear. Implicit submission (Enter in a single-line field) fires aclickon the default button; the test can't press a real Enter key, so it relies on:defaultfor which button that is. - Clicking Clear submitted the form with
event.submitterset to Clear. So with Enter, Clear's handler empties the field and then the form submits, the opposite of what the user wanted. - After setting Clear's type to
button,:defaultmoves to Search, and Enter now submits with Search. That's whytype="button"belongs on every toggle, "add row" or "clear" button in a form.
Learn more: Buttons in forms.
<header>Site name</header>
<main>
<article>
<header><h2>Post title</h2></header>
<p>…</p>
<footer>Posted 20 Oct</footer>
</article>
<section><h2>Comments</h2></section>
<section aria-labelledby="rel"><h2 id="rel">Related</h2></section>
</main>
<footer>© 2026</footer>A screen-reader user opens the landmarks list. What's in it?
Show answer
Four: banner, main, the region "Related", and contentinfo. Only the page-level header and footer become the banner and contentinfo landmarks; inside an article they're section header and footer parts, not landmarks. A section is only a region landmark when it has an accessible name, so the unnamed Comments section is a plain generic box. The article itself is exposed as an article, which isn't a landmark either.
<header id="h-page">Site name</header>
<main id="m">
<article id="art">
<header id="h-art"><h2>Post title</h2></header>
<p>Text of the post.</p>
<footer id="f-art">Posted 20 Oct</footer>
</article>
<section id="s-comments"><h2>Comments</h2></section>
<section id="s-related" aria-labelledby="rel"><h2 id="rel">Related</h2></section>
</main>
<footer id="f-page">© 2026</footer>- The top-level
<header>and<footer>come out asbannerandcontentinfo: the landmarks that let a user jump to the site header or footer. - The same elements inside the
articlecome out assectionheaderandsectionfooterin Chrome 155. That context rule keeps a page with twenty cards from having twenty banners. <main>is themainlandmark, and there should be one per page. Thearticlehas rolearticle, useful for "next article" navigation but not in the landmarks list.- The Comments
sectionhas no accessible name, so it'sgeneric; the Related one is named by its heading througharia-labelledby, which makes it aregioncalled "Related". The test counted exactly four landmark roles.
Learn more: Landmarks and sectioning.
<head>
<script src="analytics.js" async></script>
<script src="vendor.js" defer></script>
<script src="app.js" defer></script>
</head>When does each script run, and why is this the right attribute for each?
Show answer
Both attributes download the script in parallel with parsing; they differ in when it runs. defer scripts run after the whole document is parsed, in document order, just before DOMContentLoaded, so vendor.js always runs before app.js and both can use the full DOM. An async script runs as soon as it arrives, interrupting the parser if it's still going, in no guaranteed order, which suits independent code like analytics. A plain <script src> without either blocks parsing while it downloads and runs, and module scripts behave like defer by default.
- The parser meets all three tags at the top of the page and starts all three downloads at once, without stopping: that's what both attributes buy over a plain script.
analytics.jsruns the moment its download finishes. That might be before or after the body is parsed, and before or after the other two, so it must not depend on the DOM being complete or onvendor.js.vendor.jsandapp.jswait until parsing is done, then run in the order they appear, even ifapp.jsfinished downloading first. Soapp.jscan rely on whatvendor.jsdefines and on every element in the page.DOMContentLoadedfires after the deferred scripts have run;asyncscripts don't hold it up. Neither attribute has any effect on an inline<script>withoutsrc(a module inline script is the exception: it's deferred).- If you changed
app.jstoasync, it could run beforevendor.jsand fail on an undefined global, intermittently, depending on network timing.
Learn more: The document skeleton and How a browser turns HTML into a page.