Every Mother

How to Make Elementor Gallery Captions Use Proper Heading Tags (H2), Without Editing Theme Files

H

If you’ve added a Basic Image Gallery in Elementor and are using the caption field, you may notice that the caption text is output as a regular <span> or <p> tag. That means it doesn’t count as a heading for SEO or accessibility, even if the caption visually looks like one.

Caption for the Elementor Basic Image Gallery is set to Attachment Caption.

Someone asked in the Reddit Elementor community forum:

“I would like to make the titles of my galleries register as headings (specifically H2), for SEO purposes. Elementor has an HTML Tag dropdown for many widgets, but not for the Media Gallery. Is there a way to convert the caption into an H2 without editing core PHP?”

The good news: Yes, there is a clean, update-safe method that requires no theme editing and works even on WordPress.com.


Why CSS Alone Can’t Solve This

First, let’s address why CSS alone can’t solve this issue. In this case, CSS only changes how text looks; it does not and can not change the semantic HTML tag to H2.

So, although it’s tempting to try something like the following CSS, Google and screen readers still see the caption title as a non-heading.

.wp-caption-text.gallery-caption {
  display: block;
  font-size: 2rem;
  font-weight: bold;
}


So for SEO and accessibility, we need to actually convert the caption into a <h2> element in the HTML.


The Clean, Updatable Solution: Convert Captions to <h2> Using JavaScript

To convert the caption in a <h2> element, we’ll use a small script that:

  • Finds gallery captions (.wp-caption-text.gallery-caption)
  • Creates an <h2>
  • Copies the existing caption text and classes
  • Replaces the original element and preserves its styling

This avoids editing Elementor or theme files, so it’s safe through updates.


Step-by-Step Instructions

1. Go to Code Snippets and Add New (or install the plugin if needed)

Create a new snippet named:
Convert Elementor Basic Gallery Captions to H2

Set it to Run on Frontend.

2. Paste in this code:

add_action( 'wp_footer', function() {
    ?>
    <script>
    document.addEventListener("DOMContentLoaded", function () {
      const captions = document.querySelectorAll(".wp-caption-text.gallery-caption");

      captions.forEach(function (caption) {
        const h2 = document.createElement("h2");

        // Preserve any classes for styling consistency
        caption.classList.forEach(cls => h2.classList.add(cls));

        // Preserve caption text/content
        h2.innerHTML = caption.innerHTML;

        // Replace caption with h2
        caption.replaceWith(h2);
      });
    });
    </script>
    <?php
});

Save and Activate the snippet.

That’s it. Your gallery captions are now real, semantic <h2> headings.

Note: Selective targeting (meaning only run on certain pages): You already placed this in a WP Code Snippet hooked to wp_footer. If you want further limits (e.g., only a specific page ID or Elementor template), add a conditional in PHP before printing the script.


Dig Deeper into the javascript

document.addEventListener("DOMContentLoaded", function () {

What it does: Adds an event listener to the document that waits for the DOMContentLoaded event.
Why: DOMContentLoaded fires when the HTML has been parsed and the DOM tree is ready (but before images and some other resources necessarily finish loading). Running the code here ensures the gallery caption elements exist in the DOM before we try to query them.
Note: Using this is safer than running the script immediately in the <head> because elements may not exist yet. If the script is printed at the bottom of the page (like wp_footer), you could sometimes skip this listener, but keeping it is defensive and reliable.

const captions = document.querySelectorAll(“.wp-caption-text.gallery-caption”);

What it does: Uses querySelectorAll to find all elements that match the CSS selector .wp-caption-text.gallery-caption.
Result type: captions is a NodeList (array-like collection) of matching elements. It can be empty (no matches).
Why this selector: It matches elements that have both classes wp-caption-text and gallery-caption.
Note: querySelectorAll accepts any CSS selector, so you can make it more specific if you want (e.g., .my-gallery .wp-caption-text.gallery-caption).

captions.forEach(function (caption) {

What it does: Iterates over every node in the captions NodeList and runs the provided function for each one.
Compatibility: Modern browsers support forEach on NodeList. If you needed IE11 support you’d convert to an array first (Array.prototype.forEach.call(...)) but for most modern sites this is fine.

const h2 = document.createElement(“h2”);

What it does: Creates a new DOM element: an <h2> tag.
Why: We need a real heading element to replace the original caption element so the document structure is semantic.

caption.classList.forEach(cls => h2.classList.add(cls));

What it does: Iterates over every class on the original caption element and copies it onto the new <h2>.
Why: This preserves any CSS styling tied to those classes so the new <h2> looks the same as the old caption.
Important detail: classList is a DOMTokenList and its forEach method enumerates each class name (string). This keeps original class ordering and duplicates are ignored by classList.add.

h2.innerHTML = caption.innerHTML;

What it does: Copies the inner HTML content of the original caption into the new <h2> — including any inline markup (emphasis tags, links, basic inline HTML).
Why: Keeps the caption text and any inline formatting exactly as it was.
Security note / caveat: Using innerHTML will copy any HTML inside the caption, including potentially unsafe HTML if your captions are coming from untrusted sources.
If your captions are plain text or you want to be safer, use h2.textContent = caption.textContent; which copies only text (no HTML).
If your captions contain safe markup that you intentionally want preserved (links, <em>, etc.) then innerHTML is appropriate.

caption.replaceWith(h2);

What it does: Replaces the original caption element in the DOM with the newly created h2 element.
Why: This removes the original (e.g., <span> or <div>) and inserts the semantic <h2> in the exact same place, preserving DOM order and layout.
Note: replaceWith is widely supported in modern browsers. If you needed older-browser fallback, you could do caption.parentNode.replaceChild(h2, caption).

}); });

Closes the function passed to .forEach.
Closes the function passed to addEventListener.


Result

BeforeAfter
Captions are spans or divsCaptions become real H2 headings
Not counted in SEO structureSearch engines recognize them properly
No screen-reader contextScreen readers announce them as sections
Risky PHP edits neededUpdate-safe snippet with no theme editing

Final Thoughts

This approach is:

✔ Safe for Elementor & theme updates
✔ Works on WordPress.com (no file access needed)
✔ Improves SEO & accessibility structure
✔ Preserves all front-end styling

If you use galleries frequently, this small enhancement makes a big difference in how search engines (and users with assistive technology) understand your page structure.

About the author

Kelly Barkhurst

Designer to Fullstack is my place to geek out and share tech solutions from my day-to-day as a graphic designer, programmer, and business owner (portfolio). I also write on Arts and Bricks, a parenting blog and decal shop that embraces my family’s love of Art and LEGO bricks!

By Kelly Barkhurst

Recent Posts

Archives

Categories