Many WordPress themes wrap widget titles in an h2, which clutters the heading outline of a page with sidebar headings at the same level as the content. This snippet changes the WordPress widget title tag from h2 to h4, or any tag you choose, in every sidebar.
The theme’s classes stay on the heading and the title text is left exactly as typed, spaces and punctuation included.
The snippet
<?php
/**
* Change the heading tag of WordPress widget titles, for example from h2 to h4.
*/
// ===== Config =====
function dpc_widget_title_tag() {
return 'h4'; // h1 to h6, p, span or div.
}
// ===== End of config =====
add_filter( 'dynamic_sidebar_params', 'dpc_widget_title_tag_params' );
function dpc_widget_title_tag_params( $params ) {
$tag = strtolower( (string) dpc_widget_title_tag() );
$allowed = array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'div' );
if ( ! in_array( $tag, $allowed, true ) || empty( $params[0] ) || ! is_array( $params[0] ) ) {
return $params;
}
// Swap only the heading tag the sidebar wraps the title in; classes and the title stay.
foreach ( array( 'before_title', 'after_title' ) as $key ) {
if ( isset( $params[0][ $key ] ) && is_string( $params[0][ $key ] ) ) {
$params[0][ $key ] = preg_replace( '#<(/?)h[1-6](?=[\s>])#i', '<$1' . $tag, $params[0][ $key ] );
}
}
return $params;
}
How it works
- Each sidebar defines the HTML around widget titles in
before_titleandafter_titlewhen it is registered, for example<h2 class="widget-title">and</h2>. - Before each widget is printed,
dynamic_sidebar()passes those settings through thedynamic_sidebar_paramsfilter. The snippet swaps the heading tag there, so the widget prints<h4 class="widget-title">around its title. - Only
h1toh6tags are replaced, and the tag from the config is checked against a short allow list, so a typo cannot break the markup.
The older version of this post used the widget_title filter and a regex that removed every space and special character from titles, so “Recent Posts” became “RecentPosts”, and it put an h4 inside the theme’s h2.
Add it with Scripts Organizer
In Scripts Organizer go to Code Blocks → Add New and give the block a name. Set Trigger location to Everywhere, tick PHP under Script location, paste the code into the PHP editor and publish. Leave Action/Hook empty. Sidebars are rendered on the front end, where Everywhere code is loaded before dynamic_sidebar() runs.
The same code also works unchanged in a small plugin or an mu-plugin.
Customise it
- Another tag: return
'h3','p'or'span'fromdpc_widget_title_tag(). - One sidebar only: at the start of
dpc_widget_title_tag_params(), return early unless$params[0]['id']is that sidebar’s ID. - Register your own sidebar with the tag you want from the start: see Register a WordPress Sidebar for Oxygen Builder.
- Hide titles on some pages: see Remove Widget Title Conditionally.
Gotchas
- Block widgets have no widget title; a Heading block inside them keeps the level you pick in the editor.
- Widgets printed with
the_widget()instead of a sidebar do not pass throughdynamic_sidebar_params. - Theme CSS that targets
.widget h2no longer matches; target.widget-titleinstead.
In the Scripts Organizer library Import it with one click from Scripts Organizer.