Skip to content

fix(core): add scope="col" to Table column headers#3715

Merged
cixzhang merged 1 commit into
facebook:mainfrom
bhamodi:a11y/table-th-scope
Jul 10, 2026
Merged

fix(core): add scope="col" to Table column headers#3715
cixzhang merged 1 commit into
facebook:mainfrom
bhamodi:a11y/table-th-scope

Conversation

@bhamodi

@bhamodi bhamodi commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

BaseTable renders every column header as a bare <th> with no scope. In packages/core/src/Table/BaseTable.tsx the per-column initialHeaderHtmlProps (around line 375) set only data-column-key (plus an optional textAlign style) — scope was never emitted, even though TableHeaderCell (packages/core/src/Table/TableHeaderCell.tsx:40) already declares scope?: 'col' | 'row' | 'colgroup' | 'rowgroup' and forwards it to the <th>.

Without scope="col", assistive technology has to guess how header cells relate to data cells. Explicit scope is the reliable, spec-blessed association: it satisfies WCAG 2.1 SC 1.3.1 Info and Relationships (Level A) via technique H63 (using the scope attribute to associate header and data cells), and matches the ARIA APG data-grid expectation that column headers carry the columnheader semantics.

The fix defaults scope: 'col' in initialHeaderHtmlProps, so every column header renders <th scope="col">.

Merge order / override

initialHeaderHtmlProps is the initial value fed into the transformHeaderCell plugin pipeline, which runs before the props are merged onto the <th>. A plugin that writes htmlProps.scope spreads over the default ({...props.htmlProps, scope: ...}), and that value is preserved through the final mergedHtmlProps spread and forwarded by TableHeaderCell. So a consumer-provided scope still wins. The new override test locks this in.

Changes

  • packages/core/src/Table/BaseTable.tsx: default scope: 'col' in initialHeaderHtmlProps.
  • packages/core/src/Table/Table.test.tsx: added two tests under BaseTable — one asserting every columnheader has scope="col", one asserting a plugin's transformHeaderCell scope override wins (queried via thead th since <th scope="row"> resolves to the rowheader role, not columnheader).

Test plan

  • node_modules/.bin/vitest run --root . packages/core/src/Table338 pass (16 files), including the 2 new tests (was 336).
  • Confirmed failing-first: ran the two new tests before the fix — both failed (getAttribute("scope") === null on the default headers), proving they catch the gap. The override test's first assertion (plugin-set scope="row") passed pre-fix, confirming the plugin scope already flows through the merge.
  • tsc --project packages/core/tsconfig.json --noEmit — clean.
  • eslint packages/core/src/Table/BaseTable.tsx packages/core/src/Table/Table.test.tsx — clean.

Notes

Found during a broader a11y audit of the Table component. Scoped to this one fix — separate findings (e.g. caption/label props, row-expansion semantics) are intentionally left out. Table.doc.mjs doesn't document header scope semantics, so no doc change was needed.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 9, 2026
@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

@bhamodi is attempting to deploy a commit to the Meta Open Source Team on Vercel.

A member of the Team first needs to authorize it.

@bhamodi bhamodi force-pushed the a11y/table-th-scope branch from ca8fb78 to 1045d87 Compare July 9, 2026 05:35

@cixzhang cixzhang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @bhamodi — scope="col" on the real column-header s, plugin overrides preserved. 🙏

@cixzhang cixzhang merged commit 2935f18 into facebook:main Jul 10, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants