WordPress.org

WordPress Developer Blog

How to extend a WordPress block

How to extend a WordPress block

If an existing WordPress block doesn’t fully meet your needs, it can be tempting to build a new one from scratch. Doing so gives you complete control over the block and what your users will see. 

While this approach is valid, it often leads to more work and ongoing maintenance, especially if you only need minor enhancements. You’ll not only have to replicate much of the existing block’s functionality, but you’ll also miss out on future updates and improvements made to the original block. This is particularly relevant for Core WordPress blocks, which are frequently updated with new features, improved user interfaces, and more.

Custom blocks are always an option, but it’s usually better to try extending existing blocks before building your own. 

There are many ways to extend a block, and this article covers most of them as well as their current limitations. To simplify the discussion, I have divided methods into two sections: the first focuses on APIs for modifying block supports, adding block styles, and registering block variations, while the second shows you how to add fully custom functionality.

Using block APIs

WordPress includes several APIs that allow you to modify blocks. The Developer Blog has a wealth of content covering these methods, so this section will provide a brief overview of each with links for further reading. 

Modifying block supports

The Block Supports API allows you to easily add or modify block features, such as alignment options, color settings, or spacing controls. With just a few lines of code, you can opt a block into or out of specific supports, which will then modify the block’s user interface in the Editor accordingly. For example, text and background color pickers would no longer be displayed for the Heading block if you removed color support.

If you are building a custom block, you should always use the available block supports before adding your own functionality. Doing so gives users a consistent editing experience and ensures your blocks integrate with WordPress features, such as Global Styles.

Supports are generally defined in a block’s block.json file. You can modify these values using either PHP or JavaScript, but often, the best method is to use the register_block_type_args filter in PHP. Doing so removes the need to enqueue an additional JavaScript file and ensures your modifications are registered on the server.

The callback function for this filter accepts two parameters:

  • $args (array): The block arguments for the registered block type.
  • $block_type (string): The block type name, including namespace.

The following code will enable duotone support on Media & Text blocks. You can place it in your theme’s functions.php file or a utility plugin.

/**
 * Enable duotone for Media & Text blocks.
 *
 * @param array  $args       The block arguments for the registered block type.
 * @param string $block_type The block type name, including namespace.
 * @return array             The modified block arguments.
 */
function example_enable_duotone_to_media_text_blocks( $args, $block_type ) {
	
	// Only add the filter to Media & Text blocks.
	if ( 'core/media-text' === $block_type ) {
		$args['supports'] ??= [];
		$args['supports']['filter'] ??= [];
		$args['supports']['filter']['duotone'] = true;

		$args['selectors'] ??= [];
		$args['selectors']['filter'] ??= [];
		$args['selectors']['filter']['duotone'] = '.wp-block-media-text .wp-block-media-text__media';
	}

	return $args;
}
add_filter( 'register_block_type_args', 'example_enable_duotone_to_media_text_blocks', 10, 2 );

Once applied, you will see the Filters panel in the Settings Sidebar and the available duotone options. 

Modifying block supports is also a common method of Editor curation. It lets you restrict functionality to specific users, post types, and more. Use the links below to learn more.

Registering block styles

Block Styles API allows you to customize the visual appearance of blocks by registering different styles. These styles appear in the Settings Sidebar of the block, giving users easy access to various design options. For example, the Button block includes several built-in styles provided by WordPress.

You can register block styles in multiple ways, but a common method uses the register_block_style() PHP function. The default Twenty Twenty-Four theme includes several examples of this approach.

The following code adds a custom style that displays an asterisk above the Heading block when enabled:

if ( ! function_exists( 'twentytwentyfour_block_styles' ) ) :
	/**
	 * Register custom block styles
	 *
	 * @since Twenty Twenty-Four 1.0
	 * @return void
	 */
	function twentytwentyfour_block_styles() {
		register_block_style(
			'core/heading',
			array(
				'name'         => 'asterisk',
				'label'        => __( 'With asterisk', 'twentytwentyfour' ),
				'inline_style' => "
				.is-style-asterisk:before {
					content: '';
					width: 1.5rem;
					height: 3rem;
					background: var(--wp--preset--color--contrast-2, currentColor);
					clip-path: path('M11.93.684v8.039l5.633-5.633 1.216 1.23-5.66 5.66h8.04v1.737H13.2l5.701 5.701-1.23 1.23-5.742-5.742V21h-1.737v-8.094l-5.77 5.77-1.23-1.217 5.743-5.742H.842V9.98h8.162l-5.701-5.7 1.23-1.231 5.66 5.66V.684h1.737Z');
					display: block;
				}

				/* Hide the asterisk if the heading has no content, to avoid using empty headings to display the asterisk only, which is an A11Y issue */
				.is-style-asterisk:empty:before {
					content: none;
				}

				.is-style-asterisk:-moz-only-whitespace:before {
					content: none;
				}

				.is-style-asterisk.has-text-align-center:before {
					margin: 0 auto;
				}

				.is-style-asterisk.has-text-align-right:before {
					margin-left: auto;
				}

				.rtl .is-style-asterisk.has-text-align-left:before {
					margin-right: auto;
				}",
			)
		);
	}
endif;

add_action( 'init', 'twentytwentyfour_block_styles' );

Notice how the function accepts the block’s name (core/heading) and an array of arguments. These arguments include the name, label, and any inline styles associated with the block style. 

Once this block style is registered, the With asterisk button will appear in the Settings Sidebar for all Heading blocks. When a user chooses this option, the name is prefixed by is-style- and added as a CSS class to the block. In this case, the class will be is-style-asterisk.

Block Styles are an excellent way to extend blocks, especially with the introduction of section styles in WordPress 6.6. Several articles on the Developer Blog cover this API in greater detail, including additional ways they can be registered and used to extend blocks. Here are a few to get you started: 

The Block Styles API has some limitations, the most significant being that only one block style can be applied at a time. Although there’s an open issue in the Gutenberg repository to address this, the Beyond block styles article series on the Developer Blog explores a custom block extension as an alternative approach.

Registering block variations

The Block Variations API is the last API to be discussed in this section. Variations allow you to create different versions of existing blocks by predefining attributes or inner blocks. Unlike block styles, which rely on CSS for visual changes, block variations are more powerful, enabling you to create what appear to be entirely new blocks from a user’s perspective.

For example, in WordPress, the Row and Stack blocks are variations of the Group block. Similarly, each embed option, like YouTube Embed and Vimeo Embed, is a variation of the primary Embed block. 

Variations can be registered using JavaScript or PHP, but the most common method is the registerBlockVariation() JavaScript function. Similar to registering a block style, this function accepts the block name and an object that contains the variation’s name, title, attributes, inner blocks, and more. The complete list of properties is available in the API documentation.

The following code example demonstrates how to register a variation for the Media & Text block. This variation sets the align and backgroundColor attributes and includes two inner blocks.

import { registerBlockVariation } from '@wordpress/blocks';

registerBlockVariation(
	'core/media-text',
	{
		name: 'media-text-default',
		title: 'Media & Text (Custom)',
		attributes: {
			align: 'wide',
			backgroundColor: 'tertiary'
		},
		innerBlocks: [
			[
				'core/heading',
				{
					level: 3,
					placeholder: 'Heading'
				} 
			],
			[
				'core/paragraph',
				{
					placeholder: 'Enter content here...'
				} 
			],
		],
		isDefault: true,
	}
);

Typically, registering a block variation creates a new “block” in the Inserter with the specified customizations. However, if you set the isDefault property to true, your variation will replace the default instance of the block, as shown in the code above.

With this block variation registered, whenever a user adds a Media & Text block to a post, it will be preconfigured with the defined attributes and inner blocks.

Block variations often provide the most flexibility among the APIs discussed in this section, especially when combined with custom functionality or other APIs like block bindings. To learn more about leveraging variations in your projects, explore the following articles:

Adding custom functionality

Once you’ve explored the available APIs and still need to extend a block further, there are several filters that allow you to add fully customized functionality. The best way to explore this approach is through a practical example that you can apply to your own sites. In this section, you’ll learn how to extend the Image block in WordPress by adding a new setting and customizing its front-end markup based on that setting.

The objective

To demonstrate how to add custom functionality to an existing block, consider the following scenario: 

You want to add role="presentation" to any decorative Image block to ensure screen readers properly handle it for accessibility purposes. A decorative image does not contribute meaningful information to the content of a page or post, and screen readers should ignore it. 

Setting an image’s alt text to null is the most common method recommended by the W3C Web Accessibility Initiative (WAI) for making decorative images accessible. However, adding the role="presentation" attribute is another supported way to ensure screen readers ignore these elements.

The objective is to create a block extension plugin that accomplishes the following:

  1. Adds a block setting that stores whether the Image block is decorative.
  2. Adds a user interface in the Editor, allowing users to configure this setting.
  3. Modifies the markup of the block based on this setting.

The front-end markup for a decorative Image block should ultimately look like this:

<!-- Before -->
<figure class="wp-block-image size-full">
<img src="https://example.com/image-path.png" class="wp-image-12345" alt=""/>
</figure>

<!-- After -->
<figure class="wp-block-image size-full">
<img src="https://example.com/image-path.png" class="wp-image-12345" alt="" role="presentation"/>
</figure>

Here’s a preview of the final result in the Editor. You can also demo this functionality by clicking the ‘Live Preview’ link below.

Getting set up

There are a few prerequisites if you’d like to follow along and build this plugin yourself. Creating a block extension is similar to building a custom block, so you’ll need a development environment. Refer to the Block Development Environment documentation for more information.

Once your local WordPress environment is up and running: 

  • Navigate to the plugins folder.
  • Add a new folder named enable-decorative-images
  • Add a file called enable-decorative-images.php inside it.
  • Add the following plugin header to the file.
<?php
/**
 * Plugin Name:         Enable Decorative Images
 * Description:         Easily make Image blocks decorative.
 * Version:             0.1.0
 * Requires at least:   6.5
 */

Activate the plugin from the WordPress admin dashboard, then:

  • Add a src folder inside the main plugin folder. 
  • Add an empty index.js file inside the src folder.
  • Add a package.json file in the main plugin folder with the following content.
{
  "name": "enable-decorative-images",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  },
  "devDependencies": {
    "@wordpress/scripts": "^28.5.0"
  }
}

This is a more simplified version of package.json than you will see in most projects, but it’s all you’ll need for this example.

Save the file and run the terminal command npm install. Once the installation is complete, run npm start to initiate the build process provided by the wp-scripts package. You should see the index.js and index.asset.php files generated in a new build folder.

Next, add the following code to the enable-decorative-images.php file. This will enqueue the build files, ensure any text strings are translatable, and make the JavaScript available in the Editor.

/**
 * Enqueue Editor scripts.
 */
function example_enqueue_block_editor_assets() {
	$asset_file = include plugin_dir_path( __FILE__ ) . 'build/index.asset.php';

	wp_enqueue_script(
		'enable-decorative-images-editor-scripts',
		plugin_dir_url( __FILE__ ) . 'build/index.js',
		$asset_file['dependencies'],
		$asset_file['version']
	);

	wp_set_script_translations(
		'enable-decorative-images-editor-scripts',
		'enable-decorative-images'
	);
}
add_action( 'enqueue_block_editor_assets', 'example_enqueue_block_editor_assets' );

The basic structure of your block extension plugin is now complete.

Dynamic versus static rendering

Before extending an existing block, you should identify whether the block is rendered dynamically or statically. You can learn more about both options in the Block Editor documentation.

In brief, statically rendered blocks have their front-end HTML output fixed and stored in the database when a user saves a post. These blocks rely entirely on a save function to define their markup, which remains unchanged unless manually edited in the Block Editor. Here’s an example of a statically rendered Paragraph block.

<!-- wp:paragraph -->
<p>Paragraph blocks are statically rendered.</p>
<!-- /wp:paragraph -->

Dynamically rendered blocks, on the other hand, do not store their HTML markup in the database. Instead, they store a block representation that includes information about how the block should be rendered. These blocks typically do not have save functions, as their front-end markup is generated dynamically using PHP when the post is displayed. Here’s an example of a dynamically rendered Site Title block. Notice how there is no HTML markup.

<!-- wp:site-title {"linkTarget":"_blank"} /-->

Knowing how a block is rendered is crucial since it determines the tools and methods available for extending the block. While you can apply most techniques to both dynamic and static blocks, filters that modify a block’s save function, for instance, are only useful for static blocks.

Since Image blocks are statically rendered, there are several extensibility methods to choose from. Let’s dive in.

Adding block attributes

Your objective’s first step is to add a block setting that determines whether the Image block is decorative. Block settings are typically managed through block attributes, which are data stored in the block’s markup representation. 

For example, in the code snippet below, linkTarget is an attribute with the value _blank. This attribute controls whether the link in the Site Title block opens in a new tab.

<!-- wp:site-title {"linkTarget":"_blank"} /-->

Blocks can have multiple attributes, and adding custom attributes won’t cause any block validation errors, even if a user later disables your plugin.

For this example, the custom attribute will be named isDecorative, a boolean value that defaults to false.

You can add attributes to a block using either PHP or JavaScript filters. Since most blocks, including the Image block, are registered both on the server (PHP) and in the Editor (JavaScript), you can use either method. However, if a block is only registered on the client side, you must use JavaScript.

You can learn more about how blocks are registered in the Block Editor Handbook.

Using PHP

Several PHP filters allow you to modify a block’s metadata, including register_block_type_args, block_type_metadata_settings, and block_type_metadata

Let’s focus on the register_block_type_args filter, which is the same filter I used previously to modify block supports. As a reminder, the callback function for this filter accepts two parameters:

  • $args (array): The block arguments for the registered block type.
  • $block_type (string): The block type name, including namespace.

The following code checks if the block type is an Image block and, if so, adds the isDecorative custom attribute.

/**
 * Adds a custom 'isDecorative' attribute to all Image blocks.
 *
 * @param array  $args       The block arguments for the registered block type.
 * @param string $block_type The block type name, including namespace.
 * @return array             The modified block arguments.
 */
function example_add_attribute_to_image_blocks( $args, $block_type ) {

    // Only add the attribute to Image blocks.
    if ( $block_type === 'core/image' ) {
        if ( ! isset( $args['attributes'] ) ) {
            $args['attributes'] = array();
        }

        $args['attributes']['isDecorative'] = array(
            'type'    => 'boolean',
            'default' => false,
        );
    }

    return $args;
}
add_filter( 'register_block_type_args', 'example_add_attribute_to_image_blocks', 10, 2 );

Using JavaScript

Alternatively, you can use the blocks.registerBlockType JavaScript filter to add the isDecorative attribute to Image blocks. Like the PHP approach, the callback function accepts the following parameters: 

  • settings (Object): The block settings for the registered block type.
  • name (string): The block type name, including namespace.

To use the filter, start by importing addFilter at the top of the index.js file.

import { addFilter } from '@wordpress/hooks';

Then, add the following code at the bottom of the file and save. The build process should be running. If not, run npm start in the terminal. Note that the implementation is virtually identical to the PHP version, and you don’t need both. Use the JavaScript version for this example.  

/**
 * Adds a custom 'isDecorative' attribute to all Image blocks.
 *
 * @param {Object} settings The block settings for the registered block type.
 * @param {string} name     The block type name, including namespace.
 * @return {Object}         The modified block settings.
 */
function addIsDecorativeAttribute( settings, name ) {

	// Only add the attribute to Image blocks.
	if ( name === 'core/image' ) {
		settings.attributes = {
			...settings.attributes,
			isDecorative: {
				type: 'boolean',
				default: false,
			},
		};
	}

	return settings;
}

addFilter(
	'blocks.registerBlockType',
	'developer-hours-examples/add-is-decorative-attribute',
	addIsDecorativeAttribute
);

Adding the Editor user interface

Once you have added the isDefault attribute, you need a way for users to toggle this setting on or off in the Editor. The user interface (UI) for this type of setting should be placed in the Settings Sidebar of each Image block, and it must be done using JavaScript. 

You can do so by filtering the Image block’s Edit component with the editor.BlockEdit filter, along with a few additional WordPress components that will be used to build out the UI. You will need: 

  • InspectorControls: A component that renders block-specific settings in the sidebar.
  • PanelBody: A component used within InspectorControls to group related UI controls in a collapsible container for better organization.
  • PanelRow: A component used within PanelBody that applies consistent styling.
  • ToggleControl: A form component that allows users to toggle boolean settings.
  • ExternalLink: A utility component for rendering external links (optional).

Begin by updating the imports at the top of the index.js file to include the following.

import { __ } from '@wordpress/i18n';
import { addFilter } from '@wordpress/hooks';
import { InspectorControls } from '@wordpress/block-editor';
import { ExternalLink, PanelBody, PanelRow, ToggleControl } from '@wordpress/components';

Then, add the filter and the callback function below.

function addImageInspectorControls( BlockEdit ) {
	return ( props ) => {
		const { name, attributes, setAttributes } = props;

		// Early return if the block is not the Image block.
		if ( name !== 'core/image' ) {
			return <BlockEdit { ...props } />;
		}

		// Retrieve selected attributes from the block.
		const { alt, isDecorative } = attributes;

		return (
			<>
				<BlockEdit { ...props } />
				<InspectorControls>
					<PanelBody
						title={ __(
							'Accessibility',
							'enable-decorative-images'
						) }
					>
						<PanelRow>
							Add settings here...
						</PanelRow>
					</PanelBody>
				</InspectorControls>
			</>
		);
	};
}

addFilter(
	'editor.BlockEdit',
	'example/add-image-inspector-controls',
	addImageInspectorControls
);

The addImageInspectorControls function is a higher-order component that takes BlockEdit as an argument (which is a component) and returns a new component. Since you only need to modify Image blocks, if the block name does not equal core/image, the original BlockEdit component is returned.

When the filter targets an Image block, the <InspectorControls>, <PanelBody>, and <PanelRow> components are used to add a new panel to the Settings Sidebar. The result will look like this.

The editor.BlockEditor filter lets you modify the properties of the BlockEdit component, wrap it in additional markup, add functionality to the Settings Sidebar, and more. However, it does not allow you to fundamentally change the markup the BlockEdit component renders itself. This limitation and others related to block extensibility will be discussed later in this article. 

Next, let’s add a ToggleControl component to the panel so that users can set the isDisabled attribute.

<PanelBody
	title={ __(
		'Accessibility',
		'enable-decorative-images'
	) }
>
	<PanelRow>
		<ToggleControl
			label={ __(
				'Image is decorative',
				'enable-decorative-images'
			) }
			checked={ isDecorative }
			onChange={ () => {
				setAttributes( {
					isDecorative: ! isDecorative,
					alt: ! isDecorative ? '' : alt,
				} );
			} }
			help={ helpText }
		/>
	</PanelRow>
</PanelBody>

Note that setAttributes is used to update both the isDecorative and alt attributes based on the current value of isDecorative. The alt attribute controls the alternative text for the Image block. An image should not have alternative text if it’s meant to be decorative. Therefore, when a user toggles the Image is decorative option, any existing alternative text for the Image block will be automatically removed.

The helpText variable has not yet been defined. This is optional and depends on the message you want to display alongside the toggle. Below is an example that uses the ExternalLink component to render a stylized link within the help text.

const helpText = (
	<>
		{ __(
			"Decorative images don't add information to the content of a page. Enabling removes alternative text and sets the image's role to presentation. ",
			'enable-decorative-images'
		) }
		<ExternalLink
			href={
				'https://www.w3.org/WAI/tutorials/images/decorative/'
			}
		>
			{ __(
				'Learn more.',
				'enable-decorative-images'
			) }
		</ExternalLink>
	</>
);

Putting it all together, the callback function for the editor.BlockEdit filter should look something like this.

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';
import { addFilter } from '@wordpress/hooks';
import { InspectorControls } from '@wordpress/block-editor';
import { ExternalLink, PanelBody, PanelRow, ToggleControl } from '@wordpress/components';

/**
 * Render the help text for the accessibility toggle.
 */
const helpText = (
	<>
		{ __(
			"Decorative images don't add information to the content of a page. Enabling removes alternative text and sets the image's role to presentation. ",
			'enable-decorative-images'
		) }
		<ExternalLink
			href={
				'https://www.w3.org/WAI/tutorials/images/decorative/'
			}
		>
			{ __(
				'Learn more.',
				'enable-decorative-images'
			) }
		</ExternalLink>
	</>
);

/**
 * Render the accessibility toggle in the Image block Settings Sidebar.
 */
function addImageInspectorControls( BlockEdit ) {
	return ( props ) => {
		const { name, attributes, setAttributes } = props;

		// Early return if the block is not the Image block.
		if ( name !== 'core/image' ) {
			return <BlockEdit { ...props } />;
		}

		// Retrieve selected attributes from the block.
		const { alt, isDecorative } = attributes;

		return (
			<>
				<BlockEdit { ...props } />
				<InspectorControls>
					<PanelBody
						title={ __(
							'Accessibility',
							'developer-hours-examples'
						) }
					>
						<PanelRow>
							<ToggleControl
								label={ __(
									'Image is decorative',
									'developer-hours-examples'
								) }
								checked={ isDecorative }
								onChange={ () => {
									setAttributes( {
										isDecorative: ! isDecorative,
										alt: ! isDecorative ? '' : alt,
									} );
								} }
								help={ helpText }
							/>
						</PanelRow>
					</PanelBody>
				</InspectorControls>
			</>
		);
	};
}

addFilter(
	'editor.BlockEdit',
	'developer-hours-examples/add-image-inspector-controls',
	addImageInspectorControls
);

Save the index.js file and make sure to run npm run build. Refresh the Editor and select an Image block. You should see the completed UI for decorative Image blocks. 

Users can now set the isDecorative attribute on any Image block. However, enabling this setting alone does not automatically add the role="presentation" attribute to the img element on the front end. An additional step is required.

Because the Image block is statically rendered, there are two ways to proceed: you can either modify the block’s saved markup using JavaScript or adjust the front-end markup using PHP. Each approach has its benefits and limitations.

Modifying a block’s saved markup

For statically rendered blocks like the Image block, the HTML markup is serialized into post_content and stored in the database via the block’s save function. This serialized markup is what ultimately gets rendered on the front end.

Here’s an example of the markup for an Image block in the Editor with the isDecorative attribute set to true. Notice that the role attribute has not yet been added:

<!-- wp:image {"id":12345,"sizeSlug":"full","linkDestination":"none","isDecorative":true} -->
<figure class="wp-block-image size-full">
	<img src="https://example.com/image-path.png" class="wp-image-12345" alt="" />
</figure>
<!-- /wp:image -->

Now you can switch to the Code Editor and manually apply role=”presentation" to the img element. But, as soon as you exit the Code Editor, a block validation error will appear. 

This error occurs because the block markup does not match the expected markup generated by the Image block’s save function. You would need to modify the save function so that it expects role="presentation" in the block markup for decorative images.

Currently, two filters allow you to modify a block’s save function, blocks.getSaveContent.extraProps and blocks.getSaveElement. Let’s examine both options. 

Using blocks.getSaveContent.extraProps

You can use the blocks.getSaveContent.extraProps filter to modify the properties of a block’s root element. In this example, the img element is nested within a figure element, making the figure the root. Therefore, this filter isn’t suitable for directly modifying the img element. However, it’s particularly useful for adding CSS classes and other attributes to static blocks, so it’s worth discussing.

The callback function for this filter receives three parameters:

  • props (Object): The current save element’s props to be modified and returned.
  • blockType (Object): A block type definition object.
  • attributes (Object): The block attributes.

Here’s an example that adds role="presentation" to the figure element of all Image blocks when the isDecorative attribute is true. The block name is retrieved from blockType to ensure this only applies to Image blocks. Add this code to the index.js file in your plugin.

/**
 * Adds the role attribute to the root element in decorative Image blocks.
 *
 * @param {Object} props       The current `save` element’s props to be modified and returned.
 * @param {Object} blockType   The block type definition object.
 * @param {Object} attributes  The block's attributes.
 * @return {Object}            The modified properties with the `role` attribute added, or the original properties if conditions are not met.
 */
function addAccessibilityRoleToImageBlocks( props, blockType, attributes ) {
	const { name } = blockType;
	const { isDecorative } = attributes;

	if ( 'core/image' === name && isDecorative ) {
		return {
			...props,
			role: 'presentation',
		};
	}

	return props;
}

addFilter(
	'blocks.getSaveContent.extraProps',
	'example/add-accessibility-role-to-image-blocks',
	addAccessibilityRoleToImageBlocks
);

Once this filter is applied, the markup in the Editor will appear as shown below, and the role attribute will be present on the front end when the post is saved. 

<!-- wp:image {"id":12345,"sizeSlug":"full","linkDestination":"none","isDecorative":true} -->
<figure class="wp-block-image size-full" role="presentation">
	<img src="https://example.com/image-path.png" class="wp-image-12345" alt="" />
</figure>
<!-- /wp:image -->

While this approach works for modifying the root element, it doesn’t address the accessibility needs of our specific example. However, keep this filter in mind for other block extensions.

Using blocks.getSaveElement

To directly modify the img element within the block, the blocks.getSaveElement filter is more appropriate. This filter allows you to update the React element returned by a block’s save function.

The callback function for this filter receives three parameters:

  • element (Object): The React element to be modified and returned.
  • blockType (Object): A block type definition object.
  • attributes (Object): The block attributes.

With access to the complete React element, you can use cloneElement to create a new element with the role attribute applied directly to the img. Although using cloneElement can be technically complex and is relatively uncommon according to the React documentation, it provides the flexibility needed for this task.

Begin by updating the imports at the top of the index.js file to include the following.

import { Children, cloneElement, isValidElement } from '@wordpress/element';

Then, you can add the following filter and callback function. 

/**
 * Adds the role attribute to the img element in decorative Image blocks.
 *
 * @param {Object} element    The React element to be modified and returned.
 * @param {Object} blockType  A block type definition object.
 * @param {Object} attributes The block attributes.
 * @return {Object}           The modified React element with updated img roles.
 */
function addAccessibilityRoleToImages( element, blockType, attributes ) {
	const { name } = blockType;
	const { isDecorative } = attributes;
	const elementChildren = element?.props?.children;

	const updateChildrenWithRole = ( children ) => {
		return Children.map( children, ( child ) => {
			if ( ! isValidElement( child ) ) {
				return child;
			}

			// Check if the child is of type 'img'. The Image block only has one img child.
			if ( child.type === 'img' ) {
				return cloneElement( child, { role: 'presentation' } );
			}

			// If the current child has children of its own, recurse over them.
			if ( child.props.children ) {
				return cloneElement( child, {
					children: updateChildrenWithRole( child.props.children ),
				} );
			}

			return child;
		} );
	};

	// Apply the correct role to child 'img' elements if the block is a decorative Image block.
	if ( 'core/image' === name && isDecorative && elementChildren ) {
		return cloneElement( element, {
			children: updateChildrenWithRole( elementChildren ),
		} );
	}

	return element;
}

addFilter(
	'blocks.getSaveElement',
	'example/add-accessibility-role-to-images',
	addAccessibilityRoleToImages
);

This method ensures that the role="presentation" attribute is correctly added to the img element in both the Editor and on the front end, meeting the example’s accessibility requirements.

That said, directly modifying the save function has some potentially significant drawbacks:

  • It’s often more complex to implement than PHP front-end alternatives.
  • It only works for blocks with a save function (statically rendered).
  • If the save function of the original block is updated, deprecation issues could result in block validation errors caused by your extension. 
  • Removing your extension in the future could lead to block validation errors.
  • Modifications are applied directly to the block content in the Editor and saved in the database.

The last point is both a benefit and a drawback, depending on your implementation. If you later update your extension in a way that changes the block markup, these changes won’t be reflected on the front end automatically. You can only modify the Image block content in the database via the Editor. So, you would need to edit each post with a modified block, resolve validation errors, and resave the post.

Therefore, it’s important to weigh these pros and cons when developing your projects. The blocks.getSaveElement filter is likely an excellent choice for simple extensions like the one in this post, but you may want to use alternatives in other scenarios.

Modifying a block’s front-end markup

The next approach to consider involves focusing solely on the front-end output of blocks, bypassing the Editor’s content altogether. You can modify the front-end markup of any block, whether it’s dynamically or statically rendered, using the render_block and render_block_{namespace/block} PHP filters. If you only need to target a specific block type, such as the Image block in this example, use the render_block_{namespace/block} filter. 

Both filters allow you to customize the block’s output by providing three parameters to the callback function:

  • $block_content (string): The block’s HTML content.
  • $block (array): The block’s complete data, including its name and attributes.
  • $instance (WP_Block): The block instance.

The $block_content variable stores the block’s HTML, which you can manipulate using DOMDocument, the HTML API, or other methods. The $block variable includes the block’s name and attributes, which is particularly important for checking conditions, such as whether an Image block is decorative.

In this example, $instance isn’t required, but it’s available if you need it for more complex extensions.

Now, let’s use the HTML API to add role="presentation" to the img element if the isDefault attribute is true, and for good measure, ensure that the alt attribute is null. Add the following code to the main plugin file.

/**
 * Adds the role attribute and removes alt text in decorative Image blocks.
 *
 * @param string $block_content The original HTML content of the block.
 * @param array  $block         The block details, including attributes.
 * @return string               The modified block content with the decorative role applied, or the original content if not decorative.
 */
function example_add_decorative_role_to_image_block( $block_content, $block ) {

    $is_decorative = $block['attrs']['isDecorative'] ?? false;

    // Only apply the modifications if the image is decorative.
    if ( $is_decorative ) {

        // Modify the img attributes using the HTML API.
        $processor = new WP_HTML_Tag_Processor( $block_content );

        if ( $processor->next_tag( 'img' ) ) {
            $processor->set_attribute( 'alt', '' );
            $processor->set_attribute( 'role', 'presentation' );
        }

        return $processor->get_updated_html();
    }

    return $block_content;
}
add_filter( 'render_block_core/image', 'example_add_decorative_role_to_image_block', 10, 2 );

Here’s the resulting HTML markup for the Image block on the front end: 

<figure class="wp-block-image size-full">
     <img src="https://example.com/image-path.png" class="wp-image-12345" alt="" role="presentation"/>
</figure>

When you modify a block’s front-end markup, the markup stored in the database and displayed in the Editor remains unchanged. This means that if your extension is ever disabled, the block will gracefully lose the added functionality without causing any block validation errors in the Editorβ€”an issue that can occur when modifying a block’s save function, as previously discussed. Additionally, this method works for all blocks, regardless of how they are rendered, so you will see it used more often in practice.

However, a downside to this approach is that the block markup in the Editor does not reflect your modifications. While this often doesn’t matter, there are situations where it might. 

Consider a plugin that performs a pre-publish accessibility check on each post. This check scans the post content for accessibility issues, such as ensuring that decorative images have the correct role attribute. In this case, you might prefer to use the blocks.getSaveElement JavaScript filter to ensure the Editor’s markup includes the modification.

Using both methods for static blocks is also an option, so choose the best approach for your project. 

Current limitations

While this article has demonstrated many ways to extend a WordPress block, it’s important to be aware of a few limitations. The most significant limitation is that you cannot modify a block’s Edit function.

One of the Block Editor’s greatest strengths is that it provides users with a visual representation of how their content will appear on the front end. A block’s Edit function controls how the block looks in the Editor, including all the interfaces that make the block editable. However, this function has no direct connection to the actual markup rendered on the site’s front end.

For example, in the screenshot below, notice that the img element includes alt text even though no alternative text is set on the block, and the Image block is marked as decorative. This img element is also wrapped in an additional div that provides drag handles for resizing the image.

Earlier, you used the editor.BlockEdit filter to add the user interface for the decorative image toggle. The callback function accepted the <BlockEdit> component, representing the Edit component of the filtered block. Since this component is not fully extensible, there are certain modifications you cannot make in the Editor.

This may be fine, depending on the block extension you want to build. For example, in this article, the inability to add role="presentation" to the img element or set alt to null in the Editor has little impact on the user experience, as the modification doesn’t visually change the block. However, for other extensions that modify a block’s markup or add elements within a block, the fact that you cannot make these changes in the Editor can be limiting.

Additionally, you’ll find a Settings panel in the Image block’s Settings Sidebar. Ideally, the Image is decorative toggle would be placed within this panel, likely below the Alternative text field. However, this isn’t currently possible. You can only add new content using the <InspectorControls> component; modifying or extending existing settings within the panel is not supported. The same limitation applies to the <BlockControls> component, which is used to add functionality to a block’s toolbar.

Another limitation is the inability to control the order in which your custom panel appears in the Settings Sidebar. This is due to how SlotFill currently works in the Editor. For more information on this, check out the open pull request in the Gutenberg repository.

That said, don’t let these limitations discourage you from exploring block extensions before jumping to custom blocks. While it’s important to understand the current constraints, each new WordPress release often includes enhancements that expand block extensibility.

Closing thoughts

This article aimed to highlight the various ways you can extend WordPress blocks while also addressing some of the limitations within the Editor. Block extensibility is one of WordPress’s most powerful features, allowing developers like you to customize and enhance functionality to meet your specific needs.

If you’re interested in additional discussion on this topic, consider sharing your thoughts in the Let’s discuss Core block extensibility thread in the Gutenberg GitHub repository or in the comments below. I also encourage you to share any interesting block extensions you’ve created. Here are a few of my own, including the complete example from this article:

I also recommend checking out the recordings from past Developer Hours sessions on the topic of extending WordPress blocks. 

Props to @fabiankaegy, @juanmaguitar, @welcher, and @greenshady for providing feedback and reviewing this post.

10 responses to “How to extend a WordPress block”

  1. WebMan Design | Oliver Juhas Avatar

    Thank you for this tutorial, Nick!

    Just a little fix suggestion:
    There is one instance of `helpMessage` in the final code in “Adding the Editor user interface” section that should be `helpText` I think.

    1. Nick Diego Avatar

      Thanks for catching that! All fixed. πŸ’ͺ

  2. Anh Tran Avatar

    This is a very useful tutorial! I like extending blocks via block styles and variations. This is the first time I know about custom settings for blocks. Looks like it can open a lot of possibilitiee.

  3. Anthony Moore Avatar

    Great tutorial as always, Nick!

    I noticed that in your “example_enable_duotone_to_media_text_blocks” function, you will want to change your condition to “if ( ‘core/media-text’ === $block_type ) {“

    1. Nick Diego Avatar

      Copy and paste strikes again! Thanks, all fixed.

  4. Huan Avatar

    Thanks Nick! I would add a limitation on block variations, supports, and filters:

    – You cannot implement them specifically for your plugin without modifying core blocks in user/client’s block editor that you don’t intend to modify outside of your plugin.

    For example, I only want to apply `core/column` block variations to blocks in my plugin. But no, that modification gets applied to all `core/column` outside of my plugin. I hope there will be a context-aware feature so the modification is only applied to `InnerBlocks` in my plugin’s container block.

    Same thing happens with `addFilter`.

    Please correct me if I misunderstand or miss something.

    1. Nick Diego Avatar

      I replied to the linked support forum topic below, but for anyone else reading the comments here:

      To my knowledge, what you are looking for is not currently possible. There is no way to detect where a specific block is located when registering block variations and when using most filters. The blockEditor.useSetting.before filter is context aware, and you can learn more about it in this article, but it won’t solve the issue you are dealing with unfortunately.

  5. Wajid Ali Tabassum Avatar

    Hi Nick Diego, Thank you for sharing this very useful tutorial.

    I’m currently extending the core Column block and have successfully added custom settings to set a background image. However, I’m stuck on how to display the selected background image as the background of the column in the editor. Could you please help me with this?

    Thank you!

  6. Eden Avatar
    Eden

    Thanks Nick, very helpful article, however I think you should mention more about this render_block_data hook – https://developer.wordpress.org/reference/hooks/render_block_data/

Leave a Reply

Your email address will not be published. Required fields are marked *