The render_block Filter: Customising Block Output (and Why It Is Not Working)
The render_block filters let you change the HTML of any block on the front end without editing templates or replacing the block. They are the main tool for customising core blocks in block themes – and a common source of "my filter is not working" questions. This guide covers both filters, safe HTML editing with the HTML API, and the usual reasons a filter seems to do nothing.
Table of Contents
- render_block vs render_block_{name}
- Example: customising core/categories
- Editing markup safely with WP_HTML_Tag_Processor
- Using attributes and the WP_Block instance
- Why your render_block filter is not working
- Alternatives
render_block vs render_block_{name}
| Filter | Runs for | Arguments | Since |
|---|---|---|---|
render_block | Every block | $block_content, $block, $instance | WP 5.0 ($instance since 5.9) |
render_block_{$name} | One block type, e.g. render_block_core/categories | Same as above | WP 5.7 |
$block_content is the rendered HTML string, $block is the parsed block array (blockName, attrs, innerBlocks, …) and $instance is the WP_Block object, which also exposes block context. Prefer the block-specific filter: it is clearer and avoids running your callback for every block on the page.
Example: Customising core/categories
The Categories block renders a <ul class="wp-block-categories-list wp-block-categories"> (or a dropdown when "Display as dropdown" is enabled). This adds a class to the list and a heading above it:
add_filter( 'render_block_core/categories', function ( $block_content, $block ) {
// Leave the dropdown variant alone
if ( ! empty( $block['attrs']['displayAsDropdown'] ) ) {
return $block_content;
}
$p = new WP_HTML_Tag_Processor( $block_content );
if ( $p->next_tag( 'ul' ) ) {
$p->add_class( 'is-style-pill-list' );
}
return '<h2 class="widget-title">' . esc_html__( 'Topics', 'my-theme' ) . '</h2>' . $p->get_updated_html();
}, 10, 2 );
The same pattern works for any block name: render_block_core/navigation, render_block_core/post-title, render_block_core/image, or third-party names like render_block_woocommerce/product-price.
Editing Markup Safely with WP_HTML_Tag_Processor
Since WordPress 6.2 the HTML API (WP_HTML_Tag_Processor) lets you find tags and change attributes without fragile regular expressions:
add_filter( 'render_block_core/image', function ( $block_content, $block ) {
$p = new WP_HTML_Tag_Processor( $block_content );
while ( $p->next_tag( 'img' ) ) {
if ( null === $p->get_attribute( 'decoding' ) ) {
$p->set_attribute( 'decoding', 'async' );
}
}
return $p->get_updated_html();
}, 10, 2 );
It handles attribute quoting and escaping for you, and leaves the rest of the markup untouched. Use add_class(), remove_class(), set_attribute() and remove_attribute(); for structural changes (wrapping, inserting elements), plain string concatenation around the block output is usually enough.
Using Attributes and the WP_Block Instance
Attributes saved in the editor are available in $block['attrs']. Attributes left at their default value are often not stored, so always use isset()/empty() or read the computed value from $instance->attributes, which includes defaults:
add_filter( 'render_block_core/post-title', function ( $content, $block, $instance ) {
$post_id = $instance->context['postId'] ?? 0; // block context
$level = $instance->attributes['level'] ?? 2; // includes default
if ( $post_id && get_post_meta( $post_id, 'is_sponsored', true ) ) {
$content .= '<p class="sponsored-label">' . esc_html__( 'Sponsored', 'my-theme' ) . '</p>';
}
return $content;
}, 10, 3 );
Remember to pass 3 as the accepted-arguments count when you need $instance.
Why Your render_block Filter Is Not Working
- Wrong hook name. It must be
render_block_core/categorieswith a slash – notrender_block_core_categoriesorrender_block_categories. Check the exact name in the block'sblock.jsonor in the editor's code view (<!-- wp:categories /-->meanscore/categories). - You expect it to change the editor. The filter runs in PHP when the front end renders. Many blocks, including Categories, draw their editor preview with JavaScript, so the editor will not show your change.
- The output is not a block. A classic "Categories" widget (
WP_Widget_Categories) or a theme callingwp_list_categories()directly is not rendered through the block system, so no block filter runs. - Accepted-args count too low. Without
, 10, 2(or3) your callback only receives$block_content, and code that reads$block['attrs']fails silently or throws. - Not returning the content. A filter must return a string. Forgetting
returnremoves the block from the page. - Registered too late or in the wrong place. Add the filter in a plugin or the active theme's
functions.php, not inside a template or a hook that runs after rendering. - Caching. Page caches, CDN caches and object-cached fragments keep serving the old HTML; purge them after deploying.
- Another callback overrides yours. Use a later priority (e.g.
20) if a plugin also filters the same block.
Alternatives
- Block styles and theme.json for purely visual changes – see what is theme.json.
register_block_type_argsto change a block's settings or swap itsrender_callbackentirely.pre_render_blockto short-circuit rendering and return your own output before the block renders.render_block_datato modify the parsed block (attributes, inner blocks) before it is rendered.
More on hooks for block themes: the complete guide to block theme filters and hooks. Official reference: render_block on developer.wordpress.org.