Opened 2 months ago
Closed 6 days ago
#65315 closed defect (bug) (fixed)
Inserting the same image with different captions on the same page results in figcaptions with duplicate `id` elements.
| Reported by: | SteelWagstaff | Owned by: | joedolson |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.1 |
| Component: | Media | Version: | |
| Severity: | normal | Keywords: | has-patch has-unit-tests commit |
| Cc: | Focuses: | accessibility |
Description
In core WordPress, using the visual editor, when a user uploads an image to the media library and inserts it in a chapter, they have the option to provide a caption.
WordPress renders media with captions as <figure> and <figcaption> elements. If the same image is included multiple times in a chapter, the figcaptions all include the same id referencing the unique WP attachment number, even if the caption content is different. This poses a problem, since HTML expects ids to be unique on a given page. We should avoid accidentally creating an accessibility issue in the rare (but valid) situations where they want to include the same image with different captions on the same page.
I'm willing to open a patch to change the figcaption to a class.
Change History (21)
This ticket was mentioned in PR #11940 on WordPress/wordpress-develop by @SteelWagstaff.
2 months ago
#1
- Keywords has-patch has-unit-tests added
#2
@
2 months ago
PR: https://github.com/WordPress/wordpress-develop/pull/11940/
Summary of problem:
When an image is inserted with a caption, WordPress creates a [caption id="attachment_123"] shortcode
When rendered, this generates:
Figure element: <figure id="attachment_123">
Figcaption element: <figcaption id="caption-attachment-123">
If the same image (same attachment ID) is used multiple times with different captions, all figcaptions get the same ID, violating the uniqueness requirement for ID attributes within HTML documents (source)
This ticket was mentioned in Slack in #accessibility by steelwagstaff. View the logs.
2 months ago
#4
@
8 weeks ago
- Milestone Awaiting Review → 7.1
- Owner set to
- Status new → accepted
This will only impact accessibility if there's an accessibility feature that needs to uniquely target an ID; but it's certainly worth addressing, nonetheless. While WCAG 4.1.1 has been removed from the accessibility standards, this is still poor practice, and IDs should be unique.
#5
@
8 weeks ago
Thanks for your response Joe. Happy to make adjustments or discuss other approaches to this issue once the PR review begins in earnest by maintainers.
@westonruter commented on PR #11940:
7 weeks ago
#7
When reviewing this, Claude pointed out something useful:
A thought on stability of the
idfor the single-caption case (non-blocking)
Just want to flag something for consideration — not a blocker, and the approach here makes sense for the problem it's solving.
In
src/wp-includes/media.php:
if ( $atts['id'] ) { $unique_id_value = wp_unique_id( sanitize_html_class( $atts['id'] ) ); $id = 'id="' . esc_attr( $unique_id_value ) . '" '; }
Since this runs every caption's
idthroughwp_unique_id(), the suffix gets appended unconditionally — including the very common case of a single[caption id="attachment_123"]on a page, where the id was already unique. So the output shifts from:
<figure id="attachment_123">
to something like:
<figure id="attachment_1231">
…with the suffix depending on how many times
wp_unique_id()was called earlier in the request.
The reason I mention it: the
idis no longer the author-supplied value, so anything that referenced it by its literal id could be affected — e.g. in-content anchors (#attachment_123), theme/plugin CSS (#attachment_123 { … }), or JS (getElementById). Probably an edge case in practice, but worth being aware of since the id had historically been stable.
To be clear, this isn't an accessibility regression — the
aria-describedby↔ figcaptionidlinkage stays internally consistent (both come from the same$caption_id_value), so the association still resolves correctly.
If preserving backward compatibility for the common path feels worthwhile, one option would be to only disambiguate on an actual collision (suffix the 2nd+ occurrence, leave the first as-is). That's more logic than the current approach, though, and may not be worth it depending on how often duplicate captions actually occur — happy to defer to your judgment here.
It seems we should only add the unique suffix for when the ID is truly going to be a duplicate. This will preserve existing CSS which may be targeting an element by a given ID. This would preserve back-compat while also fixing the accessibility issue.
@westonruter commented on PR #11940:
6 weeks ago
#8
Looks like the unit tests need to be updated now.
@SteelWagstaff commented on PR #11940:
6 weeks ago
#9
Thanks @westonruter -- I'm traveling up to Portland today but will try to get to this when I have a spare minute. Question about approach. Right now I've been leaving the duplicate if checks, but wonder whether it might be cleaner/simpler to consolidate the logic using a single ternary to determine the caption_id_source, then use another ternary for the final unique ID. This eliminates the redundant if blocks:
if ( $atts['id'] ) {
$unique_id_value = wp_unique_id( sanitize_html_class( $atts['id'] ) );
$id = 'id="' . esc_attr( $unique_id_value ) . '" ';
}
$caption_id_source = $atts['caption_id']
? sanitize_html_class( $atts['caption_id'] )
: ( $atts['id'] ? 'caption-' . str_replace( '_', '-', sanitize_html_class( $atts['id'] ) ) : '' );
if ( $caption_id_source ) {
// Use the figure's unique ID to derive caption ID when caption_id came from id.
$caption_id_value = ( ! empty( $unique_id_value ) && 0 === strpos( $caption_id_source, 'caption-' ) )
? 'caption-' . $unique_id_value
: wp_unique_id( $caption_id_source );
$caption_id = 'id="' . esc_attr( $caption_id_value ) . '" ';
$describedby = 'aria-describedby="' . esc_attr( $caption_id_value ) . '" ';
}
For me, this feels cleaner because a single line would determines what the caption_id source should be (explicit provided value, derived from id, or empty), it removes the need to modify $attscaption_id first, it ensures that caption_id_source vs caption_id_value is obvious, and preserves the guarantee of uniqueness. Thoughts?
#10
@
6 weeks ago
I submitted https://github.com/SteelWagstaff/wordpress-develop/pull/1 to update the media caption shortcode tests for the current behavior in PR 11940.
The media test suite now passes locally:
npm run test:php -- tests/phpunit/tests/media.php
Result:
Tests: 326, Assertions: 999, Skipped: 1.
@westonruter commented on PR #11940:
6 weeks ago
#11
@SteelWagstaff That refactor seems like it could be a good idea, but let's first fix the underlying issue and get the jobs passing before we consider that refactor.
@SteelWagstaff commented on PR #11940:
6 weeks ago
#12
@westonruter I've updated the functions and checked that all tests pass. Figure and figure caption IDs are now guaranteed to be unique.
@westonruter commented on PR #11940:
6 weeks ago
#13
@SteelWagstaff Did you see my suggestions regarding the use of wp_unique_prefixed_id() and stripping off the -1?
@SteelWagstaff commented on PR #11940:
6 weeks ago
#14
@westonruter Yes, but I think I misunderstood its purpose. I just read the function description in https://developer.wordpress.org/reference/functions/wp_unique_prefixed_id/ and it now makes more sense to me. Like you, I'm trying to think of the best way to ensure unique IDs AND preserve backwards compatibility for existing CSS selectors. There have been so many suggestions going back and forth I wasn't sure where things stood.
Here's my understanding of what we want to try:
- Give each figure and figcaption a unique ID on their first usage on the page. If the same image is inserted a second time, increment the unique ID by adding
-2,-3as a suffix to each additional occurrence. - This means that we might have a figure with
id="attachment_6"and figcaption withid="caption_attachment_6". The next time it's used on the page, the id would be"attachment_6-2"and the figcaption would be"caption_attachment_ 6-2", and so on.
Does that sound right to you?
@westonruter commented on PR #11940:
6 weeks ago
#15
@SteelWagstaff yes, that sounds right. The problem with the existing wp_unique_id() is that it adds the incremented suffix irrespective of the prefix, and we want to strip off -1 from the first instance.
@joedolson commented on PR #11940:
3 weeks ago
#16
This is a good change with the suggested changes by @westonruter; but I think those changes need to be resolved before it's mergeable. It's important that existing validly unique IDs should not change, as that could cause a lot of compatibility issues with page content and custom code. Anything that wasn't already a unique ID would most likely have already been broken, so those IDs are reasonable to change, but anything that is currently unique today should stay unchanged.
Weston's suggestions should be enough to do that.
@SteelWagstaff commented on PR #11940:
3 weeks ago
#17
Thanks @joedolson. I was traveling for a conference and then away on vacation but should have time to get changed applied soon. Appreciate the encouragement and clear guidance from both of you.
@westonruter commented on PR #11940:
11 days ago
#18
I tested this and found it wasn't working as expected.
Given this post content:
[caption id="attachment_797" align="alignnone" width="300"][[Image(http://localhost:8000/wp-content/uploads/2026/07/PXL_20260304_180429713-1-300x226.webp)]] First[/caption] [caption id="attachment_797" align="alignnone" width="300"][[Image(http://localhost:8000/wp-content/uploads/2026/07/PXL_20260304_180429713-1-300x226.webp)]] Second[/caption]
It was still adding the numeric suffix to every ID:
<figure id="attachment_7971" aria-describedby="caption-attachment-7971" style="width: 300px" class="wp-caption alignnone">[[Image(http://localhost:8000/wp-content/uploads/2026/07/PXL_20260304_180429713-1-300x226.webp)]] <figcaption id="caption-attachment-7971" class="wp-caption-text">First</figcaption> </figure> <br> <figure id="attachment_7972" aria-describedby="caption-attachment-7972" style="width: 300px" class="wp-caption alignnone">[[Image(http://localhost:8000/wp-content/uploads/2026/07/PXL_20260304_180429713-1-300x226.webp)]] <figcaption id="caption-attachment-7972" class="wp-caption-text">Second</figcaption> </figure>
With the changes in 1d992f0:
<figure id="attachment_797" aria-describedby="caption-attachment-797" style="width: 300px" class="wp-caption alignnone">[[Image(http://localhost:8000/wp-content/uploads/2026/07/PXL_20260304_180429713-1-300x226.webp)]] <figcaption id="caption-attachment-797" class="wp-caption-text">First</figcaption> </figure> <br> <figure id="attachment_797-2" aria-describedby="caption-attachment-797-2" style="width: 300px" class="wp-caption alignnone">[[Image(http://localhost:8000/wp-content/uploads/2026/07/PXL_20260304_180429713-1-300x226.webp)]] <figcaption id="caption-attachment-797-2" class="wp-caption-text">Second</figcaption> </figure>
Difference:
-
.html
old new 1 <figure id="attachment_797 1" aria-describedby="caption-attachment-7971" style="width: 300px"1 <figure id="attachment_797" aria-describedby="caption-attachment-797" style="width: 300px" … … 9 <figcaption id="caption-attachment-797 1" class="wp-caption-text">First</figcaption>9 <figcaption id="caption-attachment-797" class="wp-caption-text">First</figcaption> … … 12 <figure id="attachment_797 2" aria-describedby="caption-attachment-7972" style="width: 300px"12 <figure id="attachment_797-2" aria-describedby="caption-attachment-797-2" style="width: 300px" … … 20 <figcaption id="caption-attachment-797 2" class="wp-caption-text">Second</figcaption>21 </figure> 20 <figcaption id="caption-attachment-797-2" class="wp-caption-text">Second</figcaption> 21 </figure>
So now it is working as intended.
@SteelWagstaff commented on PR #11940:
11 days ago
#19
Thanks @westonruter, appreciate your help getting this cleaned up and ready for merge!
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
Ensure that figure and figcaption elements have unique IDs when inserted into a page.
Problem:
When the same image is inserted multiple times on a page, the <figure> and <figcaption> elements receive duplicate HTML IDs. This violates HTML validity requirements (IDs must be unique) and breaks accessibility features like aria-describedby that depend on ID references
Trac ticket: https://core.trac.wordpress.org/ticket/65315#ticket
## Use of AI Tools
AI assistance: Yes
Tool(s): GitHub Copilot Pro (integrated in IDE), Skills provided by obra/superpowers
Model(s): Claude Haiku 4.5
Used for: Writing tests; code changes and comments written by me.