Build1 publisher2 min readPublished
Three tickets in one quarter trace back to a spec row built from divs
An accessibility audit, an extraction pass and an AI shopping assistant each found a different symptom in the same product comparison table. The markup underneath paired values to headers by position only.
The Engineer · Build desk

What happened
- A dev.to post walks through an e-commerce product comparison table built as a wrapper div, a row of header divs and a stack of value divs, with media queries folding it to one column on mobile.
- An accessibility audit found that a screen reader announced "Battery life" and then read two numbers back to back, with nothing in the markup tying either number to a product.
- A structured-data review found that an automated extraction pass built to feed a comparison feed could not consistently tell which value belonged to which column.
- A one-off test asked an AI shopping assistant for one product's weight and got the other product's number back.
- The post's proposed replacement is a table element with th scope="col" for each product and th scope="row" for each spec, carrying 18 hours and 14 hours in the battery row.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Table markup hands the mobile collapse problem back to whoever ships the fix, because the media queries over divs were the answer to it in the first place.
- decision An extraction suite whose fixtures give every product every spec will go green on the broken markup, so the fixture set has to include a row with a missing value before the test means anything.
- cost Working ticket by ticket buys an aria-label, a data attribute and an extraction special case, each owned by a different team, over a structure that still pairs values to headers by position.
- exposure The shopping assistant sits outside the team's pipeline, so its wrong answers cannot be special-cased the way the in-house extraction script's can.
CSS grid fills the boxes it is handed, in source order. The grid version in the post declares three of them with `grid-template-columns: 1fr 1fr 1fr` [13], matching a header row of Specification, Product A and Product B [14]. Give that row two values where three are expected and everything after the gap moves one column left, so Product B's 14 hours ends up under Product A [20]. The extraction pass had been reading by document order and column position the whole time, and it had been correct the whole time, until a product turned up with a spec the other one lacked [8].
That timing splits the three tickets. The screen reader complaint is present on every render, because the accessibility tree never had anything to compute a relationship from [11]. The extraction and assistant failures waited for a ragged row [8][9]. A team whose fixtures give every product every spec will watch its extraction tests pass and ship the same markup [21]. Three tools noticed before anyone opened the markup underneath the grid [7].
The table version declares the pairing. `<th scope="col">` and `<th scope="row">` tell the browser which cells each header governs, and that becomes part of the accessibility tree, computable by any consumer walking it [12]. Both versions render the same three columns, and the author's argument is that only one of them spares a consumer from inferring the pairing from where the boxes happen to sit [22].
The swap has a bill, and the post is straight about it. Table layout has historically fought responsive design, and collapsing a table onto a narrow viewport without JavaScript reflowing rows into cards has never been simple [15]. The div version got its mobile behaviour from media queries that fold it to a single column [2]. Whoever ships the table markup owns that fold again. For a photo grid or a blog card index the trade never comes up, because position is the only relationship those cells have [16].
The account is one developer's walkthrough on dev.to, published in English and Spanish [19], and it does not name the company or report what the three tools returned after the markup changed [23]. For the screen reader, the fix is definitional: scope attributes are what the tree is built from [12]. The extraction pass benefits if its parser reads table semantics instead of falling back on position [8]. The evidence that the shopping assistant would benefit is thinner, and it is this: the assistant failed in the same place the positional extractor did, on the same shifted row [9].
What to watch
- A no-JavaScript technique for collapsing table markup on a narrow viewport would remove the reason the div version existed.
- A follow-up reporting what the accessibility audit, the extraction pass and the assistant returned after the markup changed.
- Any assistant vendor documenting whether it reads th scope attributes or only the rendered text.