WordPress.org

WordPress Developer Blog

Curating the Editor experience with client-side filters

With each new WordPress release, the Editor’s design tools give users more power and flexibility, adding ever more ways to make content truly their own. But that power can make it tough to keep a site on brand visually and on track with an organization’s content guidelines. And the more people working on the site, the tougher that job gets.

That’s why, at the same time WordPress has been expanding users’ options, the project has been actively introducing methods to curate the Editor experience. This helps ensure that the Editor is used in a way that aligns with the goals and objectives of the site’s owners. Some industries call these curation techniques “governance.”

Today, governing the content of a website has gotten easier thanks to a new client-side filter introduced in WordPress 6.2. It’s called the blockEditor.useSetting.before filter, and it puts a level of curation in your hands that has never been possible until now.

This article will guide you through the details and explore these new capabilities.

Getting started with client-side filters

The blockEditor.useSetting.before filter is a client-side JavaScript filter that lets you set block-level theme.json settings before the Editor is rendered. And, since it operates in the client, you get much more control than you have with server-side PHP filters. For example, you can easily modify settings based on a block’s relationship to other blocks.

To begin, first import addFilter from the @wordpress/hooks package. The syntax for the function looks like this:

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

addFilter( 'hookName', 'namespace', callback, priority );

JavaScript filters in WordPress look and work a lot like PHP filters, but they do require a namespace as the second argument. The namespace uniquely identifies the callback function and can be anything you want. Priority is optional, so this article will ignore it for simplicity.

The callback function for the blockEditor.useSetting.before filter accepts four parameters. 

  • settingValue – The current value of the block setting being filtered.
  • settingName – The name of the block setting to modify.
  • clientId – The unique identifier for the block.
  • blockName – The name of the block type.

Here’s an example of how to restrict the spacing units for Column blocks to just pixels. The spacing.units setting is filtered, and the callback checks the blockName parameter to see if the current block is a Column. If it is, the function returns an array of [ 'px' ]. If not, it returns the current settingValue.

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

function restrictColumnSpacingSettings(
    settingValue,
    settingName,
    clientId,
    blockName
) {
    if ( 
        blockName === 'core/column' && 
        settingName === 'spacing.units' 
    ) {
        return [ 'px' ];
    }

    return settingValue;
}

addFilter(
    'blockEditor.useSetting.before',
    'block-curation-examples/useSetting.before/column-spacing',
    restrictColumnSpacingSettings
);

While you can make this simple change with server-side theme.json filters, or directly in a theme’s theme.json file, the blockEditor.useSetting.before filter is unique because it allows you to modify settings in many other ways: according to the block’s location, neighboring blocks, the current user’s role, and more. 

Just remember these two things when you use this filter:

  • You can only modify block settings that theme.json can define.
  • The filter will not generate new CSS variables.

The filter cannot change block attributes directly—only block settings you can define at the block level in theme.json. Many Core, and most custom, blocks include settings you can’t configure in theme.json, meaning you can’t change them with the blockEditor.useSetting.before filter.

When you define colors, font sizes, spacing, and more in theme.json, under the hood, you’re actually creating CSS variables that are then used to apply those settings. Again, the blockEditor.useSetting.before filter will not generate new CSS variables. So if you use the filter to define a color palette for a block, the colors will only work if they have already been defined in theme.json. Think of theme.json as the source of all block settings.

But even with those limitations, this client-side filter’s power and flexibility can be truly impressive. Let’s look at a few more advanced examples.

All examples in this article were designed for the Twenty Twenty-Three theme, even though most are theme-agnostic. If you see a list of font sizes or a color palette here, know they have already been defined in theme.json.

Restricting settings by block attributes

Say you want to restrict the typography settings in Heading blocks based on their levels. For instance, you may grant H1 and H2 full access to all typography settings, but limit H3 through H6 to just three font sizes and disable all the other settings. You can’t do that in theme.json. But it’s simple with the blockEditor.useSetting.before filter.

The level of a Heading block is saved as an attribute called level. So you can add a check for the current heading level in the course of filtering the typography settings. If the level is greater than or equal to 3, modify each settingValue accordingly.

Before you see how to add the heading-level check, let’s review the initial setup for disabling typography settings, except for the three predefined font sizes, across all Heading blocks. Remember that the example below assumes the Twenty Twenty-Three theme, where the font sizes are already defined in theme.json.

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

function restrictHeadingTypographySettings(
    settingValue,
    settingName,
    clientId,
    blockName
) {
    if ( blockName === 'core/heading' ) {
        // Modify these block settings.
        const modifiedBlockSettings = {
            'typography.customFontSize': false,
            'typography.fontStyle': false,
            'typography.fontWeight': false,
            'typography.letterSpacing': false,
            'typography.lineHeight': false,
            'typography.textDecoration': false,
            'typography.textTransform': false,
            'typography.fontSizes': [
                {
                    fluid: {
                        min: '1rem',
                        max: '1.125rem',
	            },
                    size: '1.125rem',
                    slug: 'medium',
                },
                {
                    fluid: {
                        min: '1.75rem',
                        max: '1.875rem',
                    },
                    size: '1.75rem',
                    slug: 'large',
                },
                {
                    fluid: false,
                    size: '2.25rem',
                    slug: 'x-large',
                },
            ],
        };

        if ( modifiedBlockSettings.hasOwnProperty( settingName ) ) {
            return modifiedBlockSettings[ settingName ];
        }
    }

    return settingValue;
}

addFilter(
    'blockEditor.useSetting.before',
    'block-curation-examples/useSetting.before/heading-typography',
    restrictHeadingTypographySettings
);

You can design a callback function in lots of ways. I chose to create an object that maps setting names to modified values. This way, when a value is being filtered, the callback function checks if the settingName of the value is in the object. If it is, the modified value is returned.

To limit these modifications to levels H3 through H6, you need to get the block’s current level attribute with getBlockAttributes, available from the @wordpress/block-editor package. That selector accepts a single parameter for the block’s unique client ID, which is already provided to the callback function as clientId.

Here’s a simplified code snippet that shows how you can use  select to return the getBlockAttributes selector from the 'core/block-editor' store and then use it to retrieve the level attribute for the selected block.

import { select } from '@wordpress/data';

const { getBlockAttributes } = select( 'core/block-editor' );

// Determine the level of the block based on its client ID.
const headingLevel = getBlockAttributes( clientId )?.level ?? 0;

The headingLevel constant looks complicated, but it retrieves the block attributes object and checks if the level property is present. If it is, the current value is returned. Otherwise, 0 is returned. The ?? is called a nullish coalescing operator

Putting it all together, the complete filter should look something like this:

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

function restrictHeadingTypographySettings(
    settingValue,
    settingName,
    clientId,
    blockName
) {
    if ( blockName === 'core/heading' ) {
        const { getBlockAttributes } = select( 'core/block-editor' );

        // Determine the level of the 'core/heading' block.
        const headingLevel = getBlockAttributes( clientId )?.level ?? 0;
        // Modify these block settings.
        const modifiedBlockSettings = {
            'typography.customFontSize': false,
            'typography.fontStyle': false,
            'typography.fontWeight': false,
            'typography.letterSpacing': false,
            'typography.lineHeight': false,
            'typography.textDecoration': false,
            'typography.textTransform': false,
            'typography.fontSizes': [
                {
                    fluid: {
                        min: '1rem',
                        max: '1.125rem',
	            },
                    size: '1.125rem',
                    slug: 'medium',
                },
                {
                    fluid: {
                        min: '1.75rem',
                        max: '1.875rem',
                    },
                    size: '1.75rem',
                    slug: 'large',
                },
                {
                    fluid: false,
                    size: '2.25rem',
                    slug: 'x-large',
                },
            ],
        };

        // Only apply setting modifications to H3-H6.
        if (
            headingLevel >= 3 &&
            modifiedBlockSettings.hasOwnProperty( settingName )
        ) {
            return modifiedBlockSettings[ settingName ];
        }
    }

    return settingValue;
}

addFilter(
    'blockEditor.useSetting.before',
    'block-curation-examples/useSetting.before/heading-typography',
    restrictHeadingTypographySettings
);

In the Editor, the available typography settings will be automatically adjusted based on the level of the Heading block.

While this example may not be relevant to your Editor curation needs, it hints at the potential of the blockEditor.useSetting.before filter. 

Next, let’s consider user permissions and conditional settings based on post type.

Restricting settings by user permissions and post type

While the Editor gives users an unparalleled level of design control for blocks, this level of freedom is not always a great thing—especially when your website has a lot of users with a wide range of roles. Administrators might need access to all the design tools, but content creators probably don’t.

In theme.json, you can restrict settings globally or at the block level. That’s great for disabling specific functionality throughout the site. But you’ll need to look elsewhere for a user-level management system for these restrictions. There are theme.json server-side PHP filters available, but those are beyond the scope of this article.

For now, maybe you want to disable border settings for everyone but Administrators, and only for posts. Border settings should stay enabled for all users when they’re editing pages or some other custom post types.

You can use the blockEditor.useSetting.before filter along with two additional tools to get this done. The canUser selector from the @wordpress/data package lets you handle permissions, while the @wordpress/editor package gives you a convenient getCurrentPostType selector.

As before, here’s a simplified snippet that shows you how  select can retrieve both selectors. This code allows you to identify the current post type and see if the current user can update settings (indicating Administrator privileges).

import { select } from '@wordpress/data';

const { canUser } = select( 'core' );
const { getCurrentPostType } = select( 'core/editor' );

// Check user permissions and get the current post type.
const canUserUpdateSettings = canUser( 'update', 'settings' );
const currentPostType = getCurrentPostType();

When you combine the power of these selectors, it’s easy to use conditional logic to enable or disable block settings based on user roles and the post type being edited. 

Putting it all together, the complete filter should look something like this:

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

function restrictBlockSettingsByUserPermissionsAndPostType(
    settingValue,
    settingName,
    clientId,
    blockName
) {
    const { canUser } = select( 'core' );
    const { getCurrentPostType } = select( 'core/editor' );

    // Check user permissions and get the current post type.
    const canUserUpdateSettings = canUser( 'update', 'settings' );
    const currentPostType = getCurrentPostType();

    // Disable block settings on these post types.
    const disabledPostTypes = [ 'post' ];

    // Disable these block settings.
    const disabledBlockSettings = [
        'border.color',
        'border.radius',
        'border.style',
        'border.width',
    ];

    if (
        ! canUserUpdateSettings &&
        disabledPostTypes.includes( currentPostType ) &&
        disabledBlockSettings.includes( settingName )
    ) {
        return false;
    }

    return settingValue;
}

addFilter(
    'blockEditor.useSetting.before',
    'block-curation-examples/useSetting.before/user-permissions-and-post-type',
    restrictBlockSettingsByUserPermissionsAndPostType
);

Now, notice that I took a different approach to modifying the settingValue in this callback function. All four border settings must be false when the conditions are met, so all I needed was a straightforward array containing each settingName

Here’s what this applied filter will look like in the Editor for users with different permissions.

Border controls are not available to Editors when the filter is applied.

Restricting settings based on block context

It was the need to define block settings in context that partially motivated the development of blockEditor.useSetting.before filter. Consider the following example. 

Suppose you have a specific style guide for buttons. When users add a button to a post or page, they should have access to the theme’s complete color palette. But when they place a button in a Cover block,  their color choices should only be black or white. Essentially, you need to restrict the color settings for Button blocks based on their location or, in other words, on their context.

Let’s ignore the contextual requirements for a second. Here’s how you would restrict the color settings for all Button blocks.

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

function restrictButtonBlockSettingsByLocation(
   settingValue,
   settingName,
   clientId,
   blockName
) {
    if ( blockName === 'core/button' ) {
        // Modify these block settings.
        const modifiedBlockSettings = {
            'color.custom': false,
            'color.customGradient': false,
            'color.defaultGradients': false,
            'color.defaultPalette': false,
            'color.gradients.theme': [],
            'color.palette.theme': [
                {
                    color: '#ffffff',
                    name: 'Base',
                    slug: 'base',
                },
                {
                    color: '#000000',
                    name: 'Contrast',
                    slug: 'contrast',
                },
            ],
        };

        return modifiedBlockSettings[ settingName ];
    }

    return settingValue;
}

addFilter(
    'blockEditor.useSetting.before',
    'block-curation-examples/useSetting.before/button-location',
    restrictButtonBlockSettingsByLocation
);

The code above will disable custom colors and gradients while simplifying the color palette to only black (Contrast) and white (Base). This callback function should look similar to the first example in this article, where a modifiedBlockSettings object contains the setting names and their new values. 

To apply these modifications based on the context of the Button block, you will need to check if the block has any parents. If it does, you can get the names of those parent blocks, and if one of the parents is a Cover, then apply the restrictions.

Thankfully, the @wordpress/block-editor package gives you the two selectors you’ll need: getBlockParents and getBlockName. Again, you will use select to retrieve both.

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

const { getBlockParents, getBlockName } = select( 'core/block-editor' );

// Get the block's parents and see if one is a 'core/cover' block.
const blockParents = getBlockParents( clientId, true );
const inCover = blockParents.some(
    ( parentId ) => getBlockName( parentId ) === 'core/cover'
);

Note that the getBlockParents selector accepts two parameters: the block client ID and a boolean parameter that specifies whether to return the block parents in ascending (true) or descending (false) order. In this example, the order is irrelevant.

With isCover defined, you can tell if the Button block is inside a Cover block. Putting it all together, the final code for the filter should look something like this:

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

function restrictButtonBlockSettingsByLocation(
    settingValue,
    settingName,
    clientId,
    blockName
) {
    if ( blockName === 'core/button' ) {
        const { getBlockParents, getBlockName } = select( 'core/block-editor' );

        // Get the block's parents and see if one's a 'core/cover' block.
        const blockParents = getBlockParents( clientId, true );
        const inCover = blockParents.some(
            ( parentId ) => getBlockName( parentId ) === 'core/cover'
        );

        // Modify these block settings.
        const modifiedBlockSettings = {
            'color.custom': false,
            'color.customGradient': false,
            'color.defaultGradients': false,
            'color.defaultPalette': false,
            'color.gradients.theme': [],
            'color.palette.theme': [
                {
                    color: '#ffffff',
                    name: 'Base',
                    slug: 'base',
                },
                {
                    color: '#000000',
                    name: 'Contrast',
                    slug: 'contrast',
                },
            ],
        };

        if ( inCover && modifiedBlockSettings.hasOwnProperty( settingName ) ) {
            return modifiedBlockSettings[ settingName ];
        }
    }

    return settingValue;
}

addFilter(
    'blockEditor.useSetting.before',
    'block-curation-examples/useSetting.before/button-location',
    restrictButtonBlockSettingsByLocation
);

And here is a before-and-after view of the Editor with this filter applied. 

The color palette for Button blocks inside Cover blocks is simplified when the filter applied.

Now that you know how to modify settings based on context, you can combine this example with user permissions, block attributes, or post types. There are so many ways to curate the Editor experience using this single filter.

For more information and a working plugin that explores the examples in this article, you can check out the Editor Curation Examples plugin on GitHub. The Block Editor Handbook also includes a fantastic resource of additional curation methods, and more are being added with each WordPress release.

So what will you build using the blockEditor.useSetting.before filter? Is there functionality that’s missing that you would like to see? Add your thoughts in the comments.

Props to @marybaum, @mburridge, @bph, @alecgeatches, @ingeniumed, and @greenshady for feedback and review.

5 responses to “Curating the Editor experience with client-side filters”

  1. Creative Andrew Avatar

    Amazing article. I am really impressed by the content quality that Nick Diego is producing not only in this piece but also in its videos and other learning resources. Keep up the good work!

    1. Nick Diego Avatar

      Thanks for the kind feedback Andrew! 🙏

  2. Iulia Cazan Avatar

    Wow! This is awesome! I have been playing for a while with the idea of restricting blocks settings based on the context and this makes it all so simple now. Thank you so much for sharing this.

  3. MCP Avatar
    MCP

    Good article. However I still don’t understand how to get rid of the Settings in core/image. I need to remove aspect ratio, width, height, limit resolution only to large. These settings are not in ‘settingName’.
    So, hard to find answers for the Block Editor issues…

    1. Nick Diego Avatar

      This is no way to remove these settings currently. Client-side filters only allow you to modify settings controlled in theme.json. Block-specific settings like those you mentioned for the Image block, cannot be managed in theme.json.

Leave a Reply to Iulia Cazan Cancel reply

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