If you’ve ever created a custom WordPress shortcode and used it in a widget, you’ve probably encountered a frustrating issue: WordPress automatically wraps your shortcode output in empty <p> tags. These unwanted paragraph tags can break your layout, add unnecessary spacing, and make your carefully crafted HTML look messy.
In this article, I’ll show you exactly why this happens and how to fix it with the correct filter priority.
The Problem
When you create a widget (particularly with the block editor’s Shortcode block) and add a simple shortcode like:
[custom_shortcode src="#" size="medium"]
You expect clean HTML output. Instead, WordPress wraps your content in paragraph tags like this:
<section id="block-3" class="widget">
<p></p>
<div class="your-container-class" data-size="medium">
<!-- Your shortcode content -->
</div>
<p></p>
</section>
Those empty <p></p> tags add unwanted margin and spacing, breaking your carefully designed layout.
Why This Happens
WordPress has a built-in filter called wpautop that automatically converts line breaks into HTML paragraph tags. This is great for blog content, but it becomes problematic when working with shortcodes that output block-level elements like <div> tags.
The block editor’s Shortcode block processes content through several filters, including wpautop, which tries to be helpful by wrapping your content in paragraph tags. Even though your shortcode outputs a complete <div> element, WordPress still adds those <p> tags around it.
The Solution: Custom Filter with Proper Priority
The fix involves creating a custom filter that strips out these unwanted tags. But here’s the critical part: filter priority matters.
Many developers try using WordPress’s built-in shortcode_unautop() function at the default priority, but this doesn’t work reliably with block-based widgets. The key is to run your cleaning filter at a very high priority (999) so it executes after all other filters have finished processing the content.
Here’s the complete solution:
class Your_Shortcode_Class {
public function __construct() {
add_shortcode( 'your_shortcode', [ $this, 'render_shortcode' ] );
// Ensure shortcodes work in widgets
add_filter( 'widget_text', 'do_shortcode', 11 );
// Handle block-based widgets (Gutenberg)
// Process shortcodes first, then clean up <p> tags at very high priority
add_filter( 'widget_block_content', 'do_shortcode', 11 );
add_filter( 'widget_block_content', [ $this, 'clean_widget_shortcode_output' ], 999 );
add_filter( 'widget_text', [ $this, 'clean_widget_shortcode_output' ], 999 );
}
/**
* Clean widget shortcode output by removing empty <p> tags
*
* @param string $content Widget content.
* @return string Cleaned content.
*/
public function clean_widget_shortcode_output( $content ) {
// Only process if your shortcode container is present in the output
if ( strpos( $content, 'your-container-class' ) === false ) {
return $content;
}
// Remove empty <p> tags (including those with whitespace)
$content = preg_replace( '/<p>(\s| )*<\/p>/i', '', $content );
// Remove <p> tags that wrap our container divs
$content = preg_replace( '/<p>(\s*)(<div[^>]*your-container-class[^>]*>)/i', '$2', $content );
$content = preg_replace( '/(<\/div>)(\s*)<\/p>/i', '$1', $content );
// Remove stray <br> tags around containers
$content = preg_replace( '/<br\s*\/?>(\s*)(<div[^>]*your-container-class)/i', '$2', $content );
$content = preg_replace( '/(<\/div>)(\s*)<br\s*\/?>/i', '$1', $content );
return $content;
}
public function render_shortcode( $atts ) {
// Your shortcode rendering logic
return '<div class="your-container-class">Your content</div>';
}
}
Understanding Filter Priority
WordPress filters execute in order of priority, from lowest to highest numbers:
- Priority 10 (default): Most filters run here, including
wpautop - Priority 11: The
do_shortcodefilter processes shortcodes - Priority 999: Our cleanup filter runs last
If you run your cleanup filter at priority 10, it will execute before wpautop adds the paragraph tags, making it ineffective. By setting it to 999, we ensure it runs after all other filters have finished, giving us the final say on the output.
Breaking Down the Regex Patterns
The cleaning function uses multiple regex patterns instead of trying to match everything at once:
- Remove empty paragraphs:
/<p>(\s| )*<\/p>/icatches<p></p>,<p> </p>, and<p> </p> - Remove opening
<p>before your container:/<p>(\s*)(<div[^>]*your-container-class[^>]*>)/istrips the opening paragraph tag that appears before your div - Remove closing
</p>after your container:/(<\/div>)(\s*)<\/p>/iremoves the closing paragraph tag after your closing div - Clean up break tags: Two patterns remove
<br>tags that appear before or after your container
This multi-pattern approach is more reliable than trying to match the entire wrapped structure in a single regex.
Implementation Tips
- Replace the identifier: Change
your-container-classto whatever unique class or attribute your shortcode uses. This ensures the filter only processes your specific shortcode output. - Add to both filters: Apply the cleanup to both
widget_block_content(Gutenberg blocks) andwidget_text(classic text widgets) to cover all use cases. - Test thoroughly: Check your widgets in different contexts – sidebars, footers, etc. – to ensure the cleanup works everywhere.
- Don’t forget
do_shortcode: Make sure you’re enabling shortcode processing inwidget_block_contentat priority 11, before your cleanup runs at priority 999.
Testing the Fix
After implementing this solution, inspect your widget output in the browser’s developer tools. You should see clean HTML like this:
<section id="block-3" class="widget">
<div class="your-container-class">
<!-- Your shortcode content -->
</div>
</section>
No more empty paragraph tags, no more unexpected spacing!
Conclusion
WordPress’s automatic paragraph insertion is helpful for blog content but can wreak havoc on shortcode output in widgets. The solution isn’t just about adding a filter – it’s about understanding filter priority and ensuring your cleanup code runs at the right time.
By running your cleanup filter at priority 999, you guarantee it executes after all other filters, giving you complete control over the final output. This simple change can save hours of debugging and ensure your shortcodes render exactly as intended.
Have you encountered this issue in your WordPress projects? Let me know in the comments how you solved it!








