Extending Core WordPress Blocks With JavaScript Filters
WordPress core blocks cover a lot of ground, but there are times when a block needs adjustments that go beyond what block styles or variations can provide. JavaScript filters, the client-side counterpart to PHP's add_filter(), let developers modify block settings, add attributes, and even change how blocks render in the editor.
To work through this, we'll look at two practical examples: removing alignment options from the Cover block and adding a size control to the Button block. A complete plugin with all the code from both examples is available on GitHub for reference.
How JavaScript Filters Differ From PHP Filters
PHP developers are familiar with hooks like add_filter(), which allow functions to receive and modify data. JavaScript filters operate similarly via the wp.hooks package, but with one notable difference: addFilter() requires a namespace as its second argument. The WordPress Handbook specifies the format vendor/plugin/function, though in practice developers often use patterns like plugin/what-filter-does or plugin/component-name/what-filter-does to keep handles unique throughout a project.
Filters can target various types of data—strings, JavaScript objects, or React components via Higher Order Components (HOCs). The latter case requires JSX, which means code must be transpiled before it can run in the browser. This adds a layer of complexity for developers coming from a PHP background who aren't yet comfortable with ES6 and React.
Example 1: Removing Cover Block Alignment Options
Suppose a project uses the Cover block strictly as a page hero that should never be aligned left, center, or right. The blocks.registerBlockType filter lets us modify the block's settings before it's registered, including the supports object that defines alignment options.
Start by adding a filter that logs the settings object and block name to the console:
const { addFilter } = wp.hooks;
function filterCoverBlockAlignments(settings, name) {
console.log({ settings, name });
return settings;
}
addFilter(
'blocks.registerBlockType',
'intro-to-filters/cover-block/alignment-settings',
filterCoverBlockAlignments,
);
Because this filter applies to all blocks, we need to check that the block name is core/cover before making any changes:
function filterCoverBlockAlignments(settings, name) {
if (name === 'core/cover') {
console.log({ settings, name });
}
return settings;
}
Alignment options are controlled through the Supports API. Setting align to true enables all alignment options, while passing an array restricts them to specific values:
supports: {
align: true
}
supports: {
align: [ 'left', 'right', 'center', 'wide', 'full' ]
}
To keep only the "Full width" alignment, we replace align: true with align: ['full'] inside the supports property:
function filterCoverBlockAlignments(settings, name) {
if (name === 'core/cover') {
return assign({}, settings, {
supports: merge(settings.supports, {
align: ['full'],
}),
});
}
return settings;
}
The lodash assign and merge methods are used here to create a new object, leaving the original settings object untouched. Mutating the original object directly would also work, but it's considered bad practice. After this change, the Cover block's toolbar should show only a single alignment toggle for "Full width."
Example 2: Adding a Size Control to the Button Block
This example requires several filters working together to add Small, Regular, and Large size options to the Button block.
Step 1: Add a Size Attribute
The first step is registering a new attribute to store the button size, using the same blocks.registerBlockType filter:
/**
* Add Size attribute to Button block
*
* @param {Object} settings Original block settings
* @param {string} name Block name
* @return {Object} Filtered block settings
*/
function addAttributes(settings, name) {
if (name === 'core/button') {
return assign({}, settings, {
attributes: merge(settings.attributes, {
size: {
type: 'string',
default: '',
},
}),
});
}
return settings;
}
addFilter(
'blocks.registerBlockType',
'intro-to-filters/button-block/add-attributes',
addAttributes,
);
This snippet alone won't change anything visible in the editor, but the attribute is necessary for the rest of the implementation.
Step 2: Add the Size Control to Inspector Controls
The editor.BlockEdit filter modifies the Inspector Controls panel on the right side of the editor. Unlike the previous filter, this one receives a React component and must return a component, requiring the HOC pattern:
/**
* Add Size control to Button block
*/
const addInspectorControl = createHigherOrderComponent((BlockEdit) => {
return (props) => {
const {
attributes: { size },
setAttributes,
name,
} = props;
if (name !== 'core/button') {
return <BlockEdit {...props} />;
}
return (
<Fragment>
<BlockEdit {...props} />
<InspectorControls>
<PanelBody
title={__('Size settings', 'intro-to-filters')}
initialOpen={false}
>
<SelectControl
label={__('Size', 'intro-to-filters')}
value={size}
options={[
{
label: __('Regular', 'intro-to-filters'),
value: 'regular',
},
{
label: __('Small', 'intro-to-filters'),
value: 'small'
},
{
label: __('Large', 'intro-to-filters'),
value: 'large'
},
]}
onChange={(value) => {
setAttributes({ size: value });
}}
/>
</PanelBody>
</InspectorControls>
</Fragment>
);
};
}, 'withInspectorControl');
addFilter(
'editor.BlockEdit',
'intro-to-filters/button-block/add-inspector-controls',
addInspectorControl,
);
The filter starts by destructuring name, setAttributes, and size from the component's props. If the block isn't core/button, it returns the original component unchanged. For Button blocks, it wraps the existing settings panel in a <Fragment /> and adds a new control component for selecting the size.
Step 3: Add a Size Class in the Editor
To visually reflect the selected size in the editor, the editor.BlockListBlock filter adds a CSS class to the block's editor wrapper:
import classnames from 'classnames';
const { addFilter } = wp.hooks;
const { createHigherOrderComponent } = wp.compose;
/**
* Add size class to the block in the editor
*/
const addSizeClass = createHigherOrderComponent((BlockListBlock) => {
return (props) => {
const {
attributes: { size },
className,
name,
} = props;
if (name !== 'core/button') {
return <BlockListBlock {...props} />;
}
return (
<BlockListBlock
{...props}
className={classnames(className, size ? `has-size-${size}` : '')}
/>
);
};
}, 'withClientIdClassName');
addFilter(
'editor.BlockListBlock',
'intro-to-filters/button-block/add-editor-class',
addSizeClass
);
This filter extracts size and className from props, checks that we're working with core/button, and uses the classnames utility to append a size-based class without manual string concatenation.
Step 4: Persist the Class in Saved Markup
Changes made in the editor need to carry through to the front end. The blocks.getSaveContent.extraProps filter hooks into the block's save() function, receiving props, block type, and attributes, and returning modified props:
import classnames from 'classnames';
const { assign } = lodash;
const { addFilter } = wp.hooks;
/**
* Add size class to the block on the front end
*
* @param {Object} props Additional props applied to save element.
* @param {Object} block Block type.
* @param {Object} attributes Current block attributes.
* @return {Object} Filtered props applied to save element.
*/
function addSizeClassFrontEnd(props, block, attributes) {
if (block.name !== 'core/button') {
return props;
}
const { className } = props;
const { size } = attributes;
return assign({}, props, {
className: classnames(className, size ? `has-size-${size}` : ''),
});
}
addFilter(
'blocks.getSaveContent.extraProps',
'intro-to-filters/button-block/add-front-end-class',
addSizeClassFrontEnd,
);
After extracting the className and size from the incoming data, the filter returns a new props object with an updated className that includes the size class when one is selected.
Step 5: Style the Custom Sizes
Front-end styles for the large and small button sizes need to be enqueued. In a plugin or theme, this is handled with wp_enqueue_style(). The editor can load the same styles via the enqueue_block_editor_assets action.
Filters Versus Custom Blocks
There's a maintenance consideration when heavily modifying core blocks with filters. Editing a block's saved markup can trigger validation errors—a mismatch between the markup in the editor and what the save() function produces.
If a project requires significant markup changes to a core block, building a custom block is often safer. The exception is when changes apply only to the front end, where PHP filters can modify markup without impacting editor validation.
Bonus: The render_block PHP Hook
For front-end-only modifications, the render_block hook filters a block's markup before it's displayed. Unlike editor-side JavaScript filters, this approach won't cause validation errors. However, it only affects the rendered output, not the editor preview.
/**
* Add button size class.
*
* @param string $block_content Block content to be rendered.
* @param array $block Block attributes.
* @return string
*/
function add_button_size_class( $block_content = '', $block = [] ) {
if ( isset( $block['blockName'] ) && 'core/button' === $block['blockName'] ) {
$defaults = ['size' => 'regular'];
$args = wp_parse_args( $block['attrs'], $defaults );
$html = str_replace(
'<div class="wp-block-button',
'<div class="wp-block-button has-size-' . esc_attr( $args['size']) . ' ',
$block_content
);
return $html;
}
return $block_content;
}
add_filter( 'render_block', 'add_button_size_class', 10, 2 );
Injecting classes via str_replace() isn't the cleanest solution, but it's sometimes the only option when working with third-party blocks where editing the source isn't feasible.
When to Reach for Block Filters
Filters are a good fit when the goal is to:
- Disable specific block features
- Add an option that doesn't change the block's markup beyond a custom class
- Modify front-end markup only, using PHP hooks
Custom blocks become preferable when the required markup changes are extensive and must work consistently in both the editor and the final output.



