Tables, images and HTML in tab content
A tab holds ordinary post content, and it is filtered the way WordPress filters post content: a fixed list of tags and attributes is kept, everything else is dropped. This page says exactly what survives, so you can tell the difference between a filter doing its job and something being broken.
What is kept and what is removed
The allow-list is WordPress's own list for post content, plus <iframe> under the conditions described below. If a tag is allowed in a post, it is allowed in a tab.
| Markup | What happens |
|---|---|
<table>, <caption>, <thead>, <tbody>, <tfoot>, <tr>, <th>, <td> | Kept, with scope, colspan, rowspan, headers and the rest of the table attributes. |
<img> | Kept, with src, alt, title, width, height, loading and usemap. |
<iframe> | Kept only when its src is an https URL on the host allow-list. Otherwise the whole element and its contents are removed. |
<video>, <audio>, <details>, <summary> | Kept. |
Headings, lists, <blockquote>, <pre>, <code>, <div>, <span> | Kept. |
class and id attributes | Kept on every element that takes them, so theme CSS written against them keeps matching. |
Inline style | Kept, but only the CSS properties WordPress allows. Most table and text properties pass; a few do not, which is covered under Tables. |
<script> | Removed. The text inside it survives as plain text. |
<style> | Removed, and the CSS inside it survives as visible text. Delete the CSS as well as the tag. |
<form>, <input>, <svg> | Removed. |
onclick and other on… attributes | Removed. |
srcset and sizes on an image | Removed. They are not on the WordPress list. |
Nothing in this list is specific to product tabs. It is the same treatment the Description field gets.
Tables, with a size chart to copy
Tables survive intact, including the header and body grouping and the accessibility attributes. Write them in the Code view of the tab editor; the Visual view does not have a table builder, and pasting a table from a word processor usually brings font and colour markup you do not want.
This is a working size chart. Paste it into the Code view of a tab and edit the numbers. It is saved unchanged, character for character.
<div style="overflow:auto">
<table class="size-chart">
<caption>Size guide (measurements in cm)</caption>
<thead>
<tr>
<th scope="col">Size</th>
<th scope="col">Chest</th>
<th scope="col">Waist</th>
<th scope="col">Sleeve</th>
</tr>
</thead>
<tbody>
<tr><th scope="row">S</th><td>92</td><td>78</td><td>62</td></tr>
<tr><th scope="row">M</th><td>100</td><td>86</td><td>64</td></tr>
<tr><th scope="row">L</th><td>108</td><td>94</td><td>66</td></tr>
<tr><th scope="row">XL</th><td>116</td><td>102</td><td>68</td></tr>
</tbody>
</table>
</div>
Three things about it are deliberate.
- The
<div style="overflow:auto">wrapper lets a wide table scroll sideways inside the tab panel instead of stretching the page on a phone. - The first cell of each row is a
<th scope="row">, so a screen reader announces the size when it reads a measurement. - The
class="size-chart"gives your theme something to style. Class names are kept, so a rule in the theme stylesheet will match.
One caveat on inline styles: WordPress keeps only the CSS properties on its own safe list. overflow is on it, which is why the wrapper above works, but overflow-x and overflow-y are not and are silently dropped. If a style you expected did not appear, that is usually the reason. Putting the rule in the theme's stylesheet and referring to it by class avoids the question entirely.
The table itself is left to your theme. The plugin ships no table CSS, so a size chart looks like every other table on your site.
Images, including the title attribute
Use the Add Media button in the tab editor. Images inserted that way keep src, alt, title, width, height, the alignment classes and the wp-image-… class.
The title attribute in particular is kept. If you add a title to an image in the media dialog, or type title="…" in the Code view, it is stored and rendered as written. Two notes on it:
- The Media Library has a Title field of its own, which is the name of the attachment, not the attribute on the tag. Changing it does not change images already inserted.
titleis not a substitute foralt. Most screen readers ignore it and it does not appear on touch devices. Writealtfor what the image shows, and usetitleonly for a genuine supplementary note.
srcset and sizes are removed from markup you paste, because they are not on the WordPress allow-list for post content.
Embedded video and the iframe allow-list
An <iframe> is kept when its src is an https URL whose host is on the allow-list. Anything else is removed entirely, including the content between the opening and closing tags. There is no partial result and no warning in the storefront; the element is simply not there.
The list that ships with the plugin is:
www.youtube.comyoutube.comwww.youtube-nocookie.complayer.vimeo.comwww.google.com
Matching is on the exact host. www.youtube.com is allowed; youtu.be, vimeo.com without the player. prefix, and anything merely containing one of these names, such as www.youtube.com.example.net, are not. The embed codes that YouTube, Vimeo and Google Maps generate use the allowed hosts, so a copied embed code normally works as it is.
http is rejected even for a host on the list. If an old embed code still starts with http://, change it to https://.
To allow another host, go to Tab Settings → Compatibility, find Allowed iframe hosts and add one host per line. Enter the bare host name, without a scheme or a path. Add only hosts you would trust to run code inside your shop's pages: an iframe can do a great deal within the frame it occupies.
For YouTube there is also the shortcode [ptstudio_youtube video_id="…"], with optional width and height. It asks WordPress for the normal oEmbed markup, and if that lookup returns nothing it falls back to a www.youtube-nocookie.com iframe.
Scripts and stylesheets
<script> tags are removed. What is not obvious is that the text inside them is not: the tags go and the JavaScript stays behind as plain text, which then appears on the product page as a paragraph of code. If a tab shows a line like document.addEventListener(…) to your customers, that is what happened. Delete the whole block, script and body together.
<style> behaves the same way. The tag is removed and the CSS inside it is left as visible text. Put the rules in your theme's stylesheet or the Customizer's Additional CSS, and give the elements in the tab a class to hook onto.
Event handler attributes such as onclick are removed from the elements that carried them, and a javascript: link loses its protocol. The element and its text stay.
If a tab genuinely needs a script — a third-party review widget, a sizing tool — the supported route is a shortcode. Register the shortcode in a small plugin or the child theme, have it enqueue its script properly, and put the shortcode in the tab. Shortcodes run when the tab renders, and the script is loaded by WordPress rather than pasted into stored content.
Shortcodes
Shortcodes are stored exactly as typed and expanded once when the tab renders. [gallery ids="1,2"], [product_page id="…"] and shortcodes from your other plugins all work inside a tab.
Once means once. An escaped shortcode written as [[gallery]] stays escaped and prints as literal text, which is the documented WordPress behaviour. Some older tab plugins ran two passes and executed it; if you moved from one of those and a doubled shortcode has stopped running, remove the outer brackets.
A shortcode that produces nothing is usually one whose plugin registers it only on certain screens. The tab plugin cannot help there; the shortcode has to exist at the moment the product page renders.
Why tab content is filtered
Tab content is stored once and then printed on a product page for every visitor, so anything executable in it runs in your customers' browsers with your shop's origin: it can read the session, alter the page, or intercept a form. The people who write tabs are often shop managers rather than administrators, and the content can also arrive from an import file or from another plugin's database rows, which nobody reviewed. Filtering on the way in means the storefront can print stored content without deciding, at render time, whether to trust it. The cost is that a handful of legitimate things — a stylesheet block, an unusual embed — need a different route, which is what the sections above describe.
When the filter does not apply to you
WordPress gives administrators on a single-site install the unfiltered_html capability, and this plugin follows the same rule as the post editor: content saved by a user who has that capability is stored exactly as typed, without the allow-list. On a multisite network nobody below a super administrator has it, and a shop manager never has it on any install.
This explains a difference that otherwise looks like a bug: the same tab content survives when you save it and loses its iframe when a colleague saves it. The filtering follows the person doing the saving, not the tab.
The same applies to migration and import. When the person running the job may post unfiltered HTML, the content is copied verbatim; otherwise the allow-list applies. The dry run counts what would change before anything is written.