Product Card Grid with Invalid Interactive Markup
MetroBikes has a product listing page with 48 product cards. Each card is a <div class="card" onclick="location.href='/p/123'"> containing a <button> labelled "Add to cart" and an <a> labelled "Details". The "Add to cart" button itself contains a nested <a> to the cart. Quantity inputs use <input type="text" id="qty"> repeated 48 times; the visible label is <span class="label">Qty</span> with no association. A promotional banner uses <marquee>. The category filter uses a <div role="tablist"> with <div role="tab"> children and no keyboard handling. QA reports: a screen reader announces "button link cart" for one control; tabbing from the first card jumps to the footer; clicking "Add to cart" sometimes navigates to the product page; the validator reports duplicate IDs and an interactive element nested inside another. The page uses one <h2> for "Products" and <h3> for product names, but has no <main> or <nav>.
1. Diagnose the invalid nesting and click behaviour. Rewrite one representative card in semantic HTML so that it is keyboard-operable and does not trigger navigation when "Add to cart" is activated.
Read the worked answer
The card is a clickable <div>, so it has no role, no keyboard focus, and no Enter/Space handling. The nested <a> inside <button> is invalid: button content is phrasing content but must not include interactive descendants. Browsers may repair the DOM unpredictably, and screen readers announce both roles. The click on "Add to cart" bubbles to the card's onclick, so the product page opens as well. Replace the wrapper with a non-clickable <li> or <article>; put a real link around the product name and image, and a separate <button type="button">Add to cart</button> as a sibling. Remove the card-level onclick entirely. If the whole card must be clickable, use a single link with a stretched pseudo-element, but keep the button outside that link's stacking context or use position: relative and raise the button above the overlay. The button must stop propagation only if a card handler still exists; better to delete the handler. Test with Tab, Enter, and Space, and confirm the accessible name is "Add to cart" without "link".
2. Fix the quantity inputs and duplicate IDs for 48 repeated controls, and explain why visible text alone is insufficient as a label.
Read the worked answer
Visible text is not a programmatic label: assistive technology needs an accessible name association, and clicking the text should focus the input. Generate a unique id per card, for example qty-<product-id>, and use <label for="qty-123">Quantity for Metro 3</label>. The label text should include the product name so a screen-reader user hearing 48 "Quantity" fields can distinguish them. Use type="number" with inputmode="numeric", min="1", and step="1", but keep server-side validation because client constraints are advisory. If the design requires compact visible text, use aria-label="Quantity for Metro 3" on the input and keep the visible span as presentational, but a real <label> is preferable. Never duplicate id="qty": duplicate IDs break for/id pairing, fragment links, and getElementById. Also wrap each control in a <form> or associate it with the correct product via a hidden input, so the submitted quantity cannot be matched to the wrong item.
3. The filter uses `role="tablist"` and `role="tab"` without keyboard handling. Decide whether to implement the tab pattern or replace it. Justify the choice and describe the required markup and keyboard behaviour if tabs remain.
Read the worked answer
role="tablist" is a promise of a specific interaction model: arrow-key navigation among tabs, aria-selected, and a associated tabpanel with aria-labelledby. Without those, the roles actively mislead users. If the filter simply navigates to a filtered URL, replace the tab roles with a <nav aria-label="Product categories"> containing links, which gives native keyboard behaviour and correct semantics. If the filter updates content in place without navigation, the tab pattern can be justified, but it must be complete: role="tablist" on the container, role="tab" on each control with aria-selected="true|false", tabindex="0" on the selected tab and tabindex="-1" on the rest, and a role="tabpanel" with aria-labelledby. Implement Left/Right arrow (and Home/End) to move selection and focus, and ensure the panel is focusable or contains the updated results. Either choice must preserve the heading outline; add <main> and a <nav> landmark, and keep one <h1> for the page. Replace <marquee> with CSS animation that respects prefers-reduced-motion, or remove the moving banner.