Skip to content

core/table Improve accessibility with semantic HTML and scope attributes#72582

Open
firestar300 wants to merge 4 commits intoWordPress:trunkfrom
firestar300:add/core-table-a11y-settings
Open

core/table Improve accessibility with semantic HTML and scope attributes#72582
firestar300 wants to merge 4 commits intoWordPress:trunkfrom
firestar300:add/core-table-a11y-settings

Conversation

@firestar300
Copy link
Copy Markdown

What?

Improves accessibility of the Table block by adding proper semantic HTML structure and ARIA attributes.

Closes #72490

This PR implements three key accessibility improvements:

  1. Changes table caption from <figcaption> to <caption> element inside the <table>
  2. Adds automatic scope="col" attribute to header cells in <thead>
  3. Preserves the scope attribute when pasting HTML tables

Why?

The Table block had several accessibility issues that prevented screen reader users from properly understanding table structure and relationships:

Issue 1: Incorrect caption element

  • Before: Caption was rendered as <figcaption> outside the <table> element
  • Problem: Screen readers cannot associate the caption with the table structure
  • After: Caption is now rendered as <caption> inside the <table> element
  • Impact: WCAG 2.1 Success Criterion 1.3.1 (Info and Relationships) - Critical

Issue 2: Missing scope attribute on header cells

  • Before: <th> elements had no scope attribute
  • Problem: Screen readers cannot determine if headers apply to rows or columns
  • After: <th> elements in <thead> now have scope="col"
  • Impact: WCAG 2.1 Success Criterion 1.3.1 (Info and Relationships) - Important

Screen reader experience comparison:

  • Before: "Header 1" → "Body 1/1" (no relationship context)
  • After: "Table with 2 columns and 2 rows, Caption" → "Column header, Header 1" → "Header 1, Body 1/1" (full context)

How?

1. Modified save.js and edit.js:

  • Changed caption rendering from <figcaption> (outside table) to <caption> (inside table)
  • Caption is now properly associated with the table element

2. Modified transforms.js:

  • Added extraction of the scope attribute when pasting HTML tables
  • The scope attribute is now preserved alongside other attributes like colspan, rowspan, and align

3. Modified state.js:

  • Updated insertRow() function to automatically add scope="col" to <th> cells in <thead> sections
  • Updated insertColumn() function to automatically add scope="col" to <th> cells in <thead> sections
  • Preserved existing behavior: <tfoot> continues to use <td> cells by default (appropriate for notes and contextual information)

4. Added caption position control:

  • Implemented a new setting in the Table block Inspector Controls to control the caption position
  • Users can now choose between "Top" or "Bottom" caption placement (default: bottom)
  • Added captionSide attribute to store the caption position preference
  • Updated styles in style.scss to use the CSS caption-side property
  • When set to "Top", the .has-caption-top class is applied to the figure wrapper, positioning the caption above the table
  • This improves flexibility for different content presentation needs while maintaining semantic HTML structure
  • The CSS caption-side property ensures the caption is properly associated with the table regardless of visual position

Testing Instructions

Test 1: Caption element position

  1. Open a post or page in the editor
  2. Insert a Table block
  3. Add a caption to the table
  4. Switch to Code Editor view
  5. Verify the markup structure:
    <figure class="wp-block-table">
      <table>
        <caption class="wp-element-caption">Table Caption</caption>
        <!-- table content -->
      </table>
    </figure>
  6. The <caption> should be inside the <table> element (not as a sibling <figcaption>)

Test 2: Creating a new table with header section

  1. Open a post or page in the editor
  2. Insert a Table block
  3. Create a table with 2 columns and 2 rows
  4. Enable the "Header section" toggle in the block settings
  5. Add a caption
  6. Switch to Code Editor view
  7. Verify the complete markup:
    <figure class="wp-block-table">
      <table class="has-fixed-layout">
        <caption class="wp-element-caption">Caption</caption>
        <thead>
          <tr>
            <th scope="col">Header 1</th>
            <th scope="col">Header 2</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>Body 1/1</td>
            <td>Body 2/1</td>
          </tr>
        </tbody>
      </table>
    </figure>

Test 3: Adding columns/rows to existing header section

  1. Create a table with header section enabled
  2. Click on a header cell
  3. Use the table toolbar to insert a column before/after
  4. Switch to Code Editor view
  5. Verify that the new <th> in the header section also has scope="col"

Test 4: Pasting HTML tables

  1. Copy the following HTML to your clipboard:
    <table>
      <caption>Product List</caption>
      <thead>
        <tr>
          <th scope="col">Name</th>
          <th scope="col">Price</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>Product A</td>
          <td>$10</td>
        </tr>
      </tbody>
    </table>
  2. Paste into the editor
  3. Switch to Code Editor view
  4. Verify that both <caption> and scope="col" attributes have been preserved

Test 5: Caption position control

  1. Create a table and add a caption
  2. In the block Inspector panel (right sidebar), locate the "Caption position" control
  3. Toggle between "Top" and "Bottom" options
  4. Verify in the editor that the caption visually moves above or below the table
  5. Switch to Code Editor view
  6. When "Top" is selected, verify the figure has the class has-caption-top:
    <figure class="wp-block-table has-caption-top">
      <table>
        <caption>Caption Text</caption>
        <!-- table content -->
      </table>
    </figure>
  7. When "Bottom" is selected (default), verify no has-caption-top class is present

Testing Instructions for Keyboard

  1. Use Tab to navigate to the table block
  2. Use arrow keys to navigate between cells
  3. Use the table toolbar (accessible via keyboard) to insert rows/columns
  4. Verify using a screen reader (NVDA, JAWS, or VoiceOver) that:
    • The caption is announced before table content
    • Header cells are announced as "Column header"
    • Data cells include their associated header (e.g., "Header 1, Body 1/1")

Screen Reader Testing (Recommended)

  • Windows + NVDA/JAWS: Navigate to table and press T to jump to tables, use Ctrl+Alt+Arrow keys to navigate cells
  • macOS + VoiceOver: Navigate to table and use VO+Right/Left Arrow to explore structure
  • Expected announcements:
    • "Table with X columns and Y rows, [Caption text]"
    • "Column header, [Header text]"
    • "[Header text], [Cell content]"

Markup

Before

<figure class="wp-block-table has-caption-top">
	<table class="has-fixed-layout">
		<thead>
			<tr>
				<th>Header 1</th>
				<th>Header 2</th>
				<th>Header 3</th>
				<th>Header 4</th>
			</tr>
		</thead>
		<tbody>
			<tr>
				<td>Body 1/1</td>
				<td>Body 2/1</td>
				<td>Body 3/1</td>
				<td>Body 4/1</td>
			</tr>
			<tr>
				<td>Body 1/2</td>
				<td>Body 2/2</td>
				<td>Body 3/2</td>
				<td>Body 4/2</td>
			</tr>
			<tr>
				<td>Body 1/3</td>
				<td>Body 2/3</td>
				<td>Body 3/3</td>
				<td>Body 4/3</td>
			</tr>
			<tr>
				<td>Body 1/4</td>
				<td>Body 2/4</td>
				<td>Body 3/4</td>
				<td>Body 4/4</td>
			</tr>
		</tbody>
	</table>
	<figcaption class="wp-element-caption">
		Caption
	</figcaption>
</figure>

After

<figure class="wp-block-table has-caption-top">
	<table class="has-fixed-layout">
		<caption class="wp-element-caption">
			Caption
		</caption>
		<thead>
			<tr>
				<th scope="col">Header 1</th>
				<th scope="col">Header 2</th>
				<th scope="col">Header 3</th>
				<th scope="col">Header 4</th>
			</tr>
		</thead>
		<tbody>
			<tr>
				<td>Body 1/1</td>
				<td>Body 2/1</td>
				<td>Body 3/1</td>
				<td>Body 4/1</td>
			</tr>
			<tr>
				<td>Body 1/2</td>
				<td>Body 2/2</td>
				<td>Body 3/2</td>
				<td>Body 4/2</td>
			</tr>
			<tr>
				<td>Body 1/3</td>
				<td>Body 2/3</td>
				<td>Body 3/3</td>
				<td>Body 4/3</td>
			</tr>
			<tr>
				<td>Body 1/4</td>
				<td>Body 2/4</td>
				<td>Body 3/4</td>
				<td>Body 4/4</td>
			</tr>
		</tbody>
	</table>
</figure>

Screenshots

image

@github-actions
Copy link
Copy Markdown

Warning: Type of PR label mismatch

To merge this PR, it requires exactly 1 label indicating the type of PR. Other labels are optional and not being checked here.

  • Type-related labels to choose from: [Type] Automated Testing, [Type] Breaking Change, [Type] Bug, [Type] Build Tooling, [Type] Code Quality, [Type] Copy, [Type] Developer Documentation, [Type] Enhancement, [Type] Experimental, [Type] Feature, [Type] New API, [Type] Task, [Type] Technical Prototype, [Type] Performance, [Type] Project Management, [Type] Regression, [Type] Security, [Type] WP Core Ticket, Backport from WordPress Core, Gutenberg Plugin, New Block.
  • Labels found: .

Read more about Type labels in Gutenberg. Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Oct 22, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: firestar300 <firestar300@git.wordpress.org>
Co-authored-by: talldan <talldanwp@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions github-actions bot added the First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository label Oct 22, 2025
@github-actions
Copy link
Copy Markdown

👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @firestar300! In case you missed it, we'd love to have you join us in our Slack community.

If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information.

@firestar300 firestar300 changed the title Add/core table a11y settings core/table Improve accessibility with semantic HTML and scope attributes Oct 22, 2025
Adds a setting to allow users to position the table caption either at the top or bottom of the table.

This change also migrates the caption element from <figcaption> to <caption> for improved accessibility.
Ensures that table header cells have the `scope` attribute set, improving accessibility by explicitly associating headers with their corresponding data cells.

This enhancement is crucial for users relying on assistive technologies, as it provides semantic information about the table structure.
@firestar300 firestar300 force-pushed the add/core-table-a11y-settings branch from 99bdc60 to 239ec04 Compare October 22, 2025 16:23
Adds the `scope="col"` attribute to table header cells in the block's test state.

This ensures that screen readers can correctly announce the column header for each cell, improving accessibility.

Also updates e2e snapshots to reflect the changes.
@firestar300 firestar300 force-pushed the add/core-table-a11y-settings branch from 239ec04 to ca85d95 Compare October 22, 2025 20:06
Adds the `scope="col"` attribute to table header cells (``) within the table header section if it's missing, enhancing accessibility.

This ensures that screen readers can correctly announce the column headers for each cell, improving the user experience for users with disabilities.
@talldan
Copy link
Copy Markdown
Contributor

talldan commented Oct 23, 2025

Thanks for the contribution. I personally think it'd be good to break this PR up into smaller PRs - one per issue mentioned rather than a big PR that tries to solve it all at ones. Some of the changes here might be easier to ship than others. But also not everything in this PR is related (they're all a11y fixes, but scope has no relationship to caption).

I've tested the PR, and I think some of my concerns about using caption mentioned in other issues are still valid. Namely that while it solves accessibility issues, it introduces others:

  • The keyboard navigation doesn't match the visible order, the caption element is focused first before the table when using a down arrow key, but it's visually after the table.
  • The PR removes the figcaption, but keeps the figure. My understanding is that this is incorrect, a table in a figure should use a figcaption for its caption (source - https://html.spec.whatwg.org/multipage/tables.html#the-caption-element, "When a table element is the only content in a figure element other than the figcaption, the caption element should be omitted in favor of the figcaption.").
  • Note: If the figure is removed, there might still be a need for a wrapping element around the table, as in the editor block wrappers are given role=document, and that shouldn't be applied to the table. The block styles will need to be thoroughly tested if this change is made.
  • In my testing there's still an issue with Voiceover/Safari where if you edit the caption's text, and then enter the table, an outdated version of the caption is announced. I think this is a browser or voiceover bug.

This issue has been around a long time, it'd definitely be great to finally solve it. Sorry that I'm mostly mentioning problems!

I personally think keeping the figure/figcaption and using aria-labelledby to associate the figcaption with the table might be an incremental improvement over what's in trunk. Labels ids are not always easy to generate for blocks though. I think the other issues are very challenging to solve and that's what's stopped people in the past.

Another option could be to make the block's markup different on the frontend and keeping the figcaption in the editor.

@firestar300
Copy link
Copy Markdown
Author

firestar300 commented Oct 24, 2025

Hi @talldan ,

Thank you for the thorough review.

Summary of Key Issues

Based on your feedback, I now understand the main problems with the current approach:

  1. HTML spec violation: Using <figure><table><caption> violates the WHATWG spec, which states that when a table is the only content in a figure, <figcaption> should be used instead of <caption>.

  2. Keyboard navigation inconsistency: I'm not sure I understand this point. We are not able to tab on a caption, the reader announces it as a whole on the table whether it's at the top or the bottom.

  3. VoiceOver caching issue: The caption text isn't updated properly in VoiceOver after editing (though this seems like a browser/AT bug rather than something we can fix).

  4. Mixing concerns: The PR combines two separate improvements (scope="col" and caption changes) that should be addressed independently.

Proposed Path Forward

PR 1: Add scope="col" attribute (Ready to proceed)

I'll create a separate, focused PR that:

  • Adds scope="col" to all <th> elements in <thead> (changes in state.js)
  • Ensures the migration in deprecated.js works correctly for existing tables
  • Updates the corresponding tests

This is a straightforward accessibility win with no structural changes. Are you comfortable with me proceeding on this immediately?

PR 2: Caption accessibility (Needs direction)

For the caption issue, I see three potential approaches, but I need your guidance on which direction makes the most sense for Gutenberg:

Option A: Keep <figure> + <figcaption>, add aria-labelledby

  • Maintains semantic correctness per HTML spec
  • Minimal breaking changes
  • Still has the keyboard navigation issue
  • Requires ID generation (could use clientId)

Option B: Remove <figure> entirely

  • Semantically correct: just <table><caption>
  • Fixes keyboard navigation order
  • Breaking change: existing styles targeting .wp-block-table figure would need migration
  • Potential issues with role="document" in the editor

Option C: Different markup for editor vs. frontend

Your suggestion about keeping the current markup in the editor while rendering proper HTML on the frontend is interesting. This could be achieved through:

  • Editor: Keep <figure> wrapper for UX/styling consistency
  • Frontend: Render without <figure>, just <table><caption>
  • This would deprecate some styles but could provide the cleanest path forward

Questions Before Proceeding

  1. Which approach do you prefer? My instinct is Option C (different editor/frontend markup) as it balances correctness with backward compatibility, but I'd defer to your experience with Gutenberg patterns.

  2. Regarding the keyboard navigation issue: Is this something we can realistically solve? Or should we document it as a known limitation when using caption-side: bottom?

Let me know your thoughts, and thanks again for the patient guidance on this!

@priethor priethor added the [Type] Enhancement A suggestion for improvement. label Oct 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[core/table] - Improve accessibility (a11y)

3 participants