SEO 및 접근성 문제는 동일한 근본 원인을 공유할 수 있습니다.

작성자

카테고리:

← 피드로
DEV Community · Franklin · 2026-09-17 개발(SW)

Structural sibling to CSS Specificity Isn’t Your Biggest Problem, same distinction, different layer.

Also available in Español

The Problem

A product comparison table ships on an e-commerce page. Two products, a column each, a row for every spec — weight, dimensions, battery life, warranty. It’s built with CSS grid: a wrapper div, a row of header divs, a stack of value divs beneath. Grid handles the alignment. Media queries collapse it to a single column on mobile. It looks exactly like a table, because visually, it is one.

Three separate reviews land on it in the same quarter. An accessibility audit flags it: a screen reader announces “Battery life” and then reads two numbers back to back, with nothing tying either number to a product. A structured-data review flags it: an automated extraction pass, built to pull spec sheets into a comparison feed, can’t consistently tell which value belongs to which column. A one-off test with an AI shopping assistant flags a third thing: asked for a product’s weight, it answers with the other product’s number.

Three teams, three tools, three tickets. Nobody notices they’re describing the same defect from three different angles, because nobody goes looking at the markup underneath the grid.

Why the Problem Exists

The grid-based approach exists for a real reason. Table layout has historically fought back against responsive design — collapsing a table gracefully onto a narrow viewport, without JavaScript reflowing the rows into cards, has never been simple. Reaching for divs and CSS grid instead of a <table> solves that problem cleanly, and for a lot of content, it’s the right call. A photo grid has no inherent relationship between its cells. Neither does a card layout for a blog index. Position is the only relationship those layouts need, and CSS grid provides it perfectly.

The First Principle

A spec sheet is different. “Battery life,” “Product A,” and “18 hours” aren’t three independent facts sitting near each other. One is a spec, one is a product, one is a value, and the whole point of the row is the relationship between them. That relationship has to live somewhere. In the grid version, it lives in one place only: the order the divs happen to appear in the source, combined with the position CSS assigns them on screen. Nothing in the markup declares it. The browser builds exactly the DOM it was handed, and that DOM has no concept of “this cell belongs to that header” — because nothing told it to.

Demonstrating the Principle

<table> was built to solve exactly this. <th scope="col"> and <th scope="row"> don’t just look like headers — they tell the browser, explicitly, which cells each header governs. That relationship becomes part of the accessibility tree, computable by any consumer walking it, not reconstructed by eye from column position:

<table>
  <caption>Product specifications</caption>
  <thead>
    <tr>
      <th scope="col">Specification</th>
      <th scope="col">Product A</th>
      <th scope="col">Product B</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Battery life</th>
      <td>18 hours</td>
      <td>14 hours</td>
    </tr>
  </tbody>
</table>

Enter fullscreen mode Exit fullscreen mode

/* Grid version: identical result on screen, no declared relationship */
.spec-row {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}

Enter fullscreen mode Exit fullscreen mode

Both render the same three columns. Only one of them tells a consumer which value goes with which header without asking it to infer that from where the boxes happen to sit.

The Pain Point

That’s the actual defect behind all three tickets. The accessibility tree had nothing to compute a relationship from, so a screen reader announced the label and then the values, in sequence, with no link between them. The extraction tooling had no declared relationship to read, so it fell back on document order and column position — which held, mostly, until a product had a spec the other one lacked, an empty cell collapsed, and every value after it in that row quietly shifted one position to the left. The AI assistant hit the same shifted alignment during that same edge case and answered with the neighboring column’s number. Three tools, three symptoms, one row that never declared what it meant.

None of this was a defect in any one team’s process. The accessibility review was right. The extraction review was right. The AI test was right. Each one was looking at the same undeclared relationship and finding a different way for its absence to show up. Patching each symptom separately — an aria-label here, a data-attribute there, a special case in the extraction script for the missing-spec scenario — would have produced three fixes bolted onto a structure that still didn’t say what it meant. Saturday’s Notes from the Pass rebuilds the same comparison component as a proper table with scope relationships in place, then runs the same three checks against it — screen reader, extraction pass, AI query — with no patch, no special case, and no shifted column left to misread.

The Broader Lesson

It’s tempting to conclude that a well-structured document satisfies every reader automatically. It doesn’t, quite. A screen reader, a search crawler, and an LLM-based tool are three different systems with three different processing pipelines, and none of them are obligated to interpret a document the way its author intended. What a declared relationship removes is narrower than that, and more useful: it removes the specific failure that comes from a consumer having to guess at something the source document could have simply stated. You can’t control how every system chooses to read a page. You can control how much of that page’s meaning depends on a reader guessing correctly.

Worth asking, the next time three separate audits come back recommending three separate fixes for the same component: whether they found three problems, or one undeclared relationship wearing three different symptoms.

원문에서 계속 ↗