By Devport Team | Last updated: 2026-10-04 | 8 min read

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

  1. render_block vs render_block_{name}
  2. Example: customising core/categories
  3. Editing markup safely with WP_HTML_Tag_Processor
  4. Using attributes and the WP_Block instance
  5. Why your render_block filter is not working
  6. Alternatives

render_block vs render_block_{name}

FilterRuns forArgumentsSince
render_blockEvery block$block_content, $block, $instanceWP 5.0 ($instance since 5.9)
render_block_{$name}One block type, e.g. render_block_core/categoriesSame as aboveWP 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

  1. Wrong hook name. It must be render_block_core/categories with a slash – not render_block_core_categories or render_block_categories. Check the exact name in the block's block.json or in the editor's code view (<!-- wp:categories /--> means core/categories).
  2. 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.
  3. The output is not a block. A classic "Categories" widget (WP_Widget_Categories) or a theme calling wp_list_categories() directly is not rendered through the block system, so no block filter runs.
  4. Accepted-args count too low. Without , 10, 2 (or 3) your callback only receives $block_content, and code that reads $block['attrs'] fails silently or throws.
  5. Not returning the content. A filter must return a string. Forgetting return removes the block from the page.
  6. 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.
  7. Caching. Page caches, CDN caches and object-cached fragments keep serving the old HTML; purge them after deploying.
  8. Another callback overrides yours. Use a later priority (e.g. 20) if a plugin also filters the same block.

Alternatives

More on hooks for block themes: the complete guide to block theme filters and hooks. Official reference: render_block on developer.wordpress.org.