Notes from the Pass: The Cell That Wasn't There
Also available in Español The Claim Being Tested Thursday's essay argued that a document's relationships have to be declared once, in structure, or every consumer reading it will reconstruct them separately —
Also available in Español
The Claim Being Tested
Thursday's essay argued that a document's relationships have to be declared once, in structure, or every consumer reading it will reconstruct them separately — and sometimes wrongly. The example was a comparison table built as a CSS grid: visually a table, structurally three divs in a row with nothing declaring which value belonged to which product.
That's a claim about documents in general. Here's what it looks like the moment one of those rows is missing a value.
The Artifact
Three products, three specs, one grid:
<div class="spec-grid">
<div class="spec-row">
<div class="spec-label">Battery life</div>
<div class="spec-value">18 hours</div>
<div class="spec-value">14 hours</div>
<div class="spec-value">20 hours</div>
</div>
<div class="spec-row">
<div class="spec-label">Water resistance</div>
<div class="spec-value">IP67</div>
<div class="spec-value">IP68</div>
</div>
</div>
.spec-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
}
Product B isn't rated for water resistance. The template's conditional — {value && <div class="spec-value">{value}</div>} — was written to avoid rendering an empty box for that case. Reasonable instinct. Nobody wants a hollow div sitting in the layout.
Look at the second row. It has three children where the first row has four: label, Product A's value, and — because Product B's div was never rendered — Product C's value, sitting in what is now the third grid slot.
What Actually Breaks
Grid auto-placement doesn't know a slot was supposed to stay empty. It only knows how many children exist and places them in order. Row one has four children, so they land in columns one through four. Row two has three, so they land in columns one through three — which means Product C's "IP68" is now sitting directly under Product B's column header.
An extraction script built to walk this table reads each .spec-value by its index within the row: position 0 maps to Product A, position 1 to Product B, position 2 to Product C. For the water resistance row, position 1 holds "IP68." The script reports it as Product B's rating. Product B doesn't have one. Product C does.
A screen reader encounters the same row, but the divs establish no table semantics and no column relationships. It may expose "Water resistance," "IP67," and "IP68" as a sequence of content, but nothing in the structure says which product each value belongs to. A listener has no structural relationship to rely on when Product B has no value at all.
An AI assistant given that same position-based extraction inherits its mistake. Asked about Product C's water resistance, it can return Product B's number — not because it reasoned incorrectly, but because the data it was handed had already attached that value to the wrong product. The row's only signal of "which value goes where" was position, and position stopped meaning what it was supposed to mean the moment a value was skipped instead of rendered empty.
The Moment It Actually Diverges
Rebuild the same row as an actual table row:
<tr>
<th scope="row">Water resistance</th>
<td>IP67</td>
<td></td>
<td>IP68</td>
</tr>
scope="row" identifies "Water resistance" as the row header. The three scope="col" headers identify the products. Together they give each data cell a declared position: the intersection of one row header and one column header, not just a slot in a flat list of children.
That's the theory. In practice, the browser finds that intersection the same way it finds a grid item's slot — by walking cells in document order and counting. Leave that second <td> out entirely, the way the grid's conditional left out its div, and "IP68" moves into Product B's column exactly as it did before: silently, with nothing on screen or in the accessibility tree announcing that a cell is missing. scope computes a relationship. It doesn't verify that the cell claiming a position actually belongs there.
The fix was never <table> as an element. It's representing the missing value explicitly — an empty <td>, or better, a real placeholder like "Not rated" — in whichever structure is being used. A grid with a properly rendered empty div in that slot stops the same shift. What the table adds, once the missing value is represented, is a relationship a screen reader or an extraction script can compute from declared header association, instead of reconstructing it from whatever happens to sit next to it.
A relationship expressed only through position survives exactly as long as every position stays filled. A relationship expressed through structure can preserve the relationship even when the value is empty — because the empty position is still represented.
What This Actually Shows
<table>
<caption>Product specifications</caption>
<thead>
<tr>
<th scope="col">Specification</th>
<th scope="col">Product A</th>
<th scope="col">Product B</th>
<th scope="col">Product C</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Water resistance</th>
<td>IP67</td>
<td>Not rated</td>
<td>IP68</td>
</tr>
</tbody>
</table>
Notice what that middle cell is doing. It isn't there to satisfy an accessibility checklist. "Not rated" is data — the absence of a water-resistance rating is a fact about Product B, and the row needed a structural place to hold it. A cell that isn't there can't carry that fact. A cell that says so can.
Run the same three checks against the corrected table. A screen reader can use the declared headers to establish the relationship between "Water resistance" and each product column; navigating the row, Product B's cell stays identifiable as Product B's cell, "Not rated" and all. An extraction pass that resolves the table's row and column headers — rather than counting populated cells — can associate Product C with "IP68" instead of whatever cell happens to precede it. A model working from that extraction answers correctly, because the data it was handed already carries the right association.
None of this required the extraction script to get smarter, or the screen reader to guess better. It required the row to stop being three boxes in a line and become a row: a structure where every value's identity is declared, not inferred from what happened to render next to it.
The claim in Thursday's essay Your SEO and Accessibility Problems Can Share the Same Root Cause wasn't abstract. This is what it looks like the first time a value goes missing.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.