WordPress.org

WordPress Developer Blog

Beyond block styles, part 3: building custom design tools

It’s time for the final leg of this journey you and I have been traveling down over the past few weeks. And I’m excited to see what you create after you read this last post in the series.

Did you miss the first two posts? No problem. Check out these links and come back to this post when you’re done:

This tutorial is the capstone of all the hard work you did in the first two lessons. You started by learning how—and why!—to use the WordPress scripts package in your theme. Then you designed some custom icon styles for the Separator block. And in that process, you hit some limitations of the Block Styles API.

So let’s kick this thing up a notch and build the custom editor control that will let your users assign an icon to the Separator block. 

You can snag the final code for this tutorial from the part-3 branch of the Beyond Block Styles GitHub repository. I recommend using it only as a supplement to this tutorial.

Remember that this is what your final product is going to look like:

Getting started

At the end of Part 2, I asked you to delete all the code from your /resources/js/editor.js file but keep all of the other code you added. If you didn’t do that then, please do it now.

Start by adding a couple of empty JavaScript files under the /resources/js folder named control-icon.js and util.js. Remember, it’s good practice to break your code down into chunks that are easier to work with.

After you added the above files, your /resources folder structure should look like this:

  • /resources
    • /js
      • const.js
      • control-icons.js
      • editor.js
      • util.js
    • /scss
      • editor.scss
      • screen.scss

Now install the WordPress icons library. Conveniently, it’s a package you can install with a single command, as we did before with the scripts. To do that, open your favorite command-line editor and navigate to your theme folder. Then enter this command:

npm install @wordpress/icons --save-dev

Now you can use the WordPress icons library in your scripts. You’ll need it later, when you add a toolbar icon.

It’s time for the really exciting part: building a custom icon picker for the Separator block.

Don’t forget to enable watch mode so that webpack compiles your code:

npm run start

Creating a custom control with JavaScript

This is the biggest part of this tutorial series. 

Is this your first time writing JavaScript for the WordPress editor? Congratulations and welcome!

There’s no rush. So take your time as you work through this article, and do yourself a favor when you see the links sprinkled throughout: go ahead and click, then read the information there in real time.

Register a gradient for each icon

In Part 2, you defined a custom set of icons within the /resources/js/const.js file, which should look like this:

export const ICONS = [
	{ value: 'floral-heart', icon: '❦' },
	{ value: 'blossom',      icon: '🌼' },
	// More icons...
];

The icon picker automatically sets a gradient background to the Separator block based on the selected icon. This means each icon needs a gradient preset assigned to it ahead of time. You can use the presets defined in your theme.json or the ones in the default WordPress theme.json. All you need is the gradient’s slug value.

Now add a new gradient property with a preset value for each icon object in your const.js file:

export const ICONS = [
	{
		value: 'floral-heart',
		icon:  '❦'
	},
	{
		value:    'blossom',
		icon:     '🌼',
		gradient: 'luminous-vivid-amber-to-luminous-vivid-orange'
	},
	{
		value:    'sun',
		icon:     '☀️',
		gradient: 'luminous-vivid-amber-to-luminous-vivid-orange'
	},
	{
		value:    'feather',
		icon:     '🪶',
		gradient: 'cool-to-warm-spectrum'
	},
	{
		value:    'fire',
		icon:     '🔥',
		gradient: 'luminous-vivid-amber-to-luminous-vivid-orange'
	},
	{
		value:    'leaves',
		icon:     '🍃',
		gradient: 'electric-grass'
	},
	{
		value:    'coffee',
		icon:     '☕',
		gradient: 'cool-to-warm-spectrum'
	},
	{
		value:    'beer',
		icon:     '🍻',
		gradient: 'cool-to-warm-spectrum'
	},
	{
		value:    'lotus',
		icon:     '🪷',
		gradient: 'luminous-dusk'
	},
	{
		value:    'melting-face',
		icon:     '🫠',
		gradient: 'luminous-vivid-amber-to-luminous-vivid-orange'
	},
	{
		value:    'guitar',
		icon:     '🎸',
		gradient: 'blush-bordeaux'
	},
	{
		value:    'pencil',
		icon:     '✏️',
		gradient: 'pale-ocean'
	},
	{
		value:    'rocket',
		icon:     '🚀',
		gradient: 'luminous-vivid-orange-to-vivid-red'
	},
	{
		value:    'clover',
		icon:     '☘️',
		gradient: 'electric-grass'
	},
	{
		value:    'star',
		icon:     '⭐',
		gradient: 'luminous-vivid-amber-to-luminous-vivid-orange'
	},
	{
		value:    'sunflower',
		icon:     '🌻',
		gradient: 'luminous-vivid-amber-to-luminous-vivid-orange'
	},
	{
		value:    'beach-umbrella',
		icon:     '⛱️',
		gradient: 'luminous-dusk'
	}
];

The icons above use the gradient presets from the WordPress theme.json file. If you do not define a gradient for an icon, it will fall back to a solid color. You defined that color in the CSS you wrote in Part 2.

Write some utility functions

Before you build your actual control, you need to write a couple of custom utility functions. You’ll store them in the new  /resources/js/utils.js file you added at the beginning of this piece.

Start by adding your dependencies to your new utils.js file. Those are the ICONS array from your const.js file and the WordPress TokenList package:

// Internal dependencies.
import { ICONS } from './const';

// WordPress dependencies.
import TokenList from '@wordpress/token-list';

The first utility function you will need will get the value of the icon assigned to the Separator block. 

The way you do that in a theme is significantly different from the way you’d do it in a plugin.

Here you’re building a feature for a theme, so you want to avoid adding custom block attributes. Plus, you already have a className attribute—which is precisely what the Block Styles API uses. So use that, and know that you’re already complying with a subset of the WordPress.org theme review guidelines.

Now add a function named getIconFromClassName() to your utils.js file:

export const getIconFromClassName = ( className ) => {
	const list = new TokenList( className );

	const style = ICONS.find( ( option ) =>
		list.contains( `is-style-icon-${ option.value }` )
	);

	return undefined !== style ? style.value : '';
};

This function creates a DOM token list of classes by passing the className variable into TokenList. Then, it searches for a class containing one of the icon values. If found, it returns the value for the icon. Else, it returns an empty string.

You will also need a function for updating the block’s className property that does not interfere with the Block Styles API or erase any CSS classes that a user might input from the UI (tip: you can find all custom classes in the Additional CSS Class(es) field under the block’s Advanced tab).

Now add the updateIconClass() function to your utils.js file:

export const updateIconClass = ( className, newIcon = '', oldIcon = '' ) => {
	const list = new TokenList( className );

	if ( oldIcon ) {
		list.remove( `is-style-icon-${ oldIcon }` );
	}

	if ( newIcon ) {
		list.add( `is-style-icon-${ newIcon }` );
	}

	return list.value;
};

Again, this function uses the TokenList function to make a list of class names. This makes it easy to target classes related to icons, removing the old icon class and adding the new.

We talked about putting utility functions in the utils.js file. Store all your common functions there—I do.

Here, you’re writing these functions specifically for the icon picker. But in a real project, I very often make these files reusable across several components. So, for instance, I’d change updateIconClass() to the more generally useful updateBlockClass().

Building a custom control

Don’t you hate it when the prep work takes so long you start to think the real work will never happen? 

Well, we’re finally done with the prep work. Let’s build that control!

When you’re building controls for the editor, I strongly recommend the Storybook tool for WordPress. It takes a lot of the heavy lifting out of selecting the right components and building the code. I used it heavily to pull together the code for this icon picker.

Now open your newly-created /resources/js/control-icons.js file. As usual, you’ll need to import several constants and functions you’ve created and a few things from WordPress, mostly components:

// Internal dependencies.
import { ICONS } from './const';
import { getIconFromClassName, updateIconClass } from './utils';

// WordPress dependencies.
import { __ } from '@wordpress/i18n';
import { starFilled } from '@wordpress/icons';

import {
	BaseControl,
	Button,
	Dropdown,
	ToolbarButton,
	__experimentalGrid as Grid
} from '@wordpress/components';

We’ll be using the __experimentaGrid component to lay the icon button out into a grid. It’s not best practice to use experimental components in production, because they could have backward-incompatible changes in the next WordPress release. 

If you do not feel comfortable with experimental features, you can recreate this component’s layout with a <div> and some custom CSS for the grid—totally up to you.

Other than imports, the control-icons.js file will contain one function, which will be its default export. Go ahead and add this:

export default ( { attributes: { className }, setAttributes } ) => {
	// Add your component code here.
};

This function destructures the props parameter, giving you attributes (an object that houses the block’s attributes) and setAttributes (a function that sets the block’s attributes). The attributes parameter is further destructured to give you access directly to the className attribute. Read more about attributes in the Block Editor Handbook.

I would rather not just dump a massive blob of JavaScript for you to copy and paste. Instead, let’s walk together through each of the next steps. You will put all new code from this section of the tutorial inside the function above

Let’s start out with an easy code snippet. 

You need a variable with the currently-assigned icon for the Separator block. So pass the className attribute to your custom getIconFromClassName() function (the one you defined in utils.js):

// Get the current icon.
const currentIcon = getIconFromClassName( className );

Now you need a function that updates the block’s attributes when a new icon is set. It will do two things:

  • Update the block’s class, which is handled by the updateIconClass() function you defined in utils.js.
  • Update the block’s gradient attribute.

The values for both of these get passed into setAttributes().

Now add the code for the onIconButtonClick() function:

// Update the icon class and gradient.
const onIconButtonClick = ( icon ) => setAttributes( {
	className: updateIconClass(
		className,
		currentIcon === icon.value ? '' : icon.value,
		currentIcon
	),
	gradient: currentIcon === icon.value || ! icon?.gradient
		? undefined
		: icon?.gradient
} );

For these next steps in building the icon picker, you will essentially be plugging your custom data into the core WordPress components.

For the icon picker, you need buttons. These let your theme users select the icon that they want for the Separator block. One of the principles of good development practice is to keep things DRY (Don’t Repeat Yourself). Instead of creating individual buttons for each icon, you’ll create a single button function using the WordPress <Button> component.

Add this code inside of the function in control-icons.js:

// Builds a menu item for an icon.
const iconButton = ( icon, index ) => (
	<Button
		key={ index }
		isPressed={ currentIcon === icon.value }
		className="theme-slug-sep-icons-picker__button"
		onClick={ () => onIconButtonClick( icon ) }
	>
		{ icon.icon ?? icon.value }
	</Button>
);

Now you will build the grid part of this. You will use the WordPress <BaseControl> component to act as a wrapping element and to output a label.

Inside the base control wrapper, you will use a <div> to output a description to help your theme users understand what the control is for. You can also leave this out.

You will also use the <Grid> component to organize each of the icon buttons. You’ll do that by looping through the ICONS array (the one you imported from const.js) and passing the data into iconButton().

Add this code inside the function in your control-icons.js file:

// Builds an icon picker in a 6-column grid.
const iconPicker = (
	<BaseControl
		className="theme-slug-sep-icons-picker"
		label={ __( 'Icons', 'theme-slug' ) }
	>
		<div className="theme-slug-sep-icons-picker__description">
			{ __( 'Pick an icon to super-charge your separator.', 'theme-slug' ) }
		</div>
		<Grid className="theme-slug-sep-icons-picker__grid" columns="6">
			{ ICONS.map( ( icon, index ) =>
				iconButton( icon, index )
			) }
		</Grid>
	</BaseControl>
);

Now there is one last piece to create the control itself. Because the icon picker control is in the block toolbar, it makes sense to use the WordPress <Dropdown> component, passing the above component code into it.

So add this to the end of the function inside your control-icons.js file:

// Returns the dropdown menu item.
return (
	<Dropdown
		className="theme-slug-sep-icons-dropdown"
		contentClassName="theme-slug-sep-icons-popover"
		popoverProps={ {
			headerTitle: __( 'Separator Icons', 'theme-slug' ),
			variant: 'toolbar'
		} }
		renderToggle={ ( { isOpen, onToggle } ) => (
			<ToolbarButton
				className="theme-slug-sep-icons-dropdown__button"
				icon={ starFilled }
				label={ __( 'Separator Icon', 'theme-slug' ) }
				onClick={ onToggle }
				aria-expanded={ isOpen }
				isPressed={ !! currentIcon }
			/>
		) }
		renderContent={ () => iconPicker }
	/>
);

Take note of the two render*() functions:

  • renderToggle() renders the button in the toolbar using the WordPress <ToolbarButton> component.
  • renderContent() renders the icon picker you made in the earlier steps.

Most of the other properties are customizable, so feel free to change them to your liking.

Showing your component in the editor

So you may have built your icon picker, but you still need to make it show up. That means you need to tell WordPress it’s there—and you do that with a filter.

Remember the /resources/js/editor.js file—the one that should be completely empty, if you followed all the steps in Part 2 of this tutorial series? It’s time to open it and add your final JavaScript code.

You’ll need a few imports here too. 

First, you need to import the component you just created. We’ll name it SeparatorIconControl. You also need the WordPress BlockControls component and addFilter() function.

Add them to your editor.js file:

// Internal dependencies.
import SeparatorIconControl from './control-icons';

// WordPress dependencies.
import { BlockControls } from '@wordpress/block-editor';
import { addFilter } from '@wordpress/hooks';

Next, you’ll need to write a filter callback function for the editor.BlockEdit filter hook. This function will do a couple of things:

  • Check if the current block is the Separator block (core/separator).
  • Add the icon control component to the block controls.

Add this to your editor.js file:

const withSeparatorIcons = ( BlockEdit ) => ( props ) => {
	return 'core/separator' === props.name ? (
		<>
			<BlockEdit { ...props } />
			<BlockControls group="other">
				<SeparatorIconControl
					attributes={ props.attributes }
					setAttributes={ props.setAttributes }
				/>
			</BlockControls>
		</>
	) : (
		<BlockEdit { ...props } />
	);
};

There are other SlotFills you can use to add custom controls. Check out Using block inspector sidebar groups for a deeper dive into other places you can put custom components.

The final piece to get a working control is to call addFilter(). Pass in the hook name, a custom namespace, and the withSeparatorIcons callback function:

addFilter(
	'editor.BlockEdit',
	'theme-slug/separator-icons',
	withSeparatorIcons
);

Now create a new post or page in the WordPress editor and add a Separator block to it. Then click the star icon in the toolbar and make sure everything works. You should see a dropdown that looks like this:

That’s not bad, but it could use a little design love. Let’s do that now.

Sprucing up the icon picker design

The last step of this process is putting the final touch on the design. A sprinkling of style rules will go a long way.

One of the reasons I chose SCSS over plain ol’ CSS for this tutorial series is that it lets you import variables, mixins, and functions (though, there are build tools that let you do all that in CSS too).

Because your component is outside WordPress, you need a way to access things like padding and margin values to maintain a consistent experience for your users. You can get this by importing the WordPress _variables.scss file.

Now open your /resources/scss/editor.scss file and add:

// WordPress imports.
@use '~@wordpress/base-styles/variables';

That file has lots of variables you can use in all kinds of projects. But for this one, you only need $grid-unit-20 for a couple of padding and margin adjustments.

It also wouldn’t hurt to bump up the size of the emoji icons themselves. I chose 20px for this, but feel free to adjust that number.

You’re down to the final bit of code. So add this SCSS to editor.scss:

.theme-slug-sep-icons-popover .components-popover__content {
	padding: variables.$grid-unit-20;
}

.theme-slug-sep-icons-picker {
	&__description {
		margin: 0 0 variables.$grid-unit-20;
	}

	&__button {
		font-size: 20px;
	}
}

Your icon picker should now look like this:

Much better, right?

The end of the journey

With that final tweak to the design, this tutorial series has concluded. Congratulations! 

You can now venture out into the wide and wonderful world that lies beyond the Block Styles API. You have all the tools you need for success.

Addendum

Part of me wants to continue this series for a fourth or even fifth round, but I’m also excited about exploring other topics here on the Developer Blog.

There is so much I didn’t cover in this three-part series: like more insights into my own development and decision-making process. Or a few more tips and tricks.

But I also wanted to respect your time. At somewhere north of 7,000 words, or the length of a 28-page college paper, you’ve probably read enough. Time to go build something cool!

But I do want to end with a few last thoughts.

Class naming

Throughout this series, I used the .is-style-{value} naming convention, which is what the Block Styles API uses. That made it easier to migrate code from Part 2 to Part 3. It can also make it easy to switch back to the Block Styles API if you want.

In my code, I would have used a .has-icon-{value} class name. That’s how I generally name things. For example, .has-text-shadow-{value} is for a text-shadow feature. .has-list-marker-{value} works for custom list markers/bullets.

You can do whatever you want—you’re not stuck with .is-style-* or any other naming convention.

Why use the className attribute?

You might be wondering why I chose to use a CSS class instead of a custom attribute. While the Theme Review Guidelines do not expressly forbid adding custom attributes to blocks, I believe using the existing className attribute is more in line with the spirit of the Functionality and Features guideline, which defines functionality that belongs in a theme vs. a plugin.

Plus, if your user changes themes, and your design disappears, the icon class on the Separator block hasn’t gone away. A site owner can still style it with CSS.

True, you do have to write a couple of helper functions. But the code is minimal and probably not any heavier than the code required for adding a custom attribute.

Other things to build

Here are some things I’ve been working on—in case you need some ideas:

  • A text-shadow selector for blocks with RichText.
  • A gradient ring/outline for Image and Post Featured Image blocks.
  • A List block dropdown for choosing the bullet style (it uses pretty much the same code from this tutorial).
  • A color variation picker that sets all colors for Group and other container-type blocks.

I could list another dozen or so ideas that I haven’t had a chance to explore yet. Plus, I don’t want to limit your imagination by rattling off features I want to build. Much better, I think, for you to set out on your own journey and make new discoveries. The one thing I ask: share your experience with others.

Resources

Resources used throughout Part 3 of this tutorial series:

Props to @marybaum, @bph, and @ndiego for testing, feedback, and review.

4 responses to “Beyond block styles, part 3: building custom design tools”

  1. Juan Pablo Avatar

    Hi, thanks for this tutorial. I have a question. It’s possible make this with SelectControl component? I don’t’ need icons, I need texts. Thanks.

    1. Justin Tadlock Avatar

      Yes, you can use any component you want. The tutorial is merely a starting point to show you how to extend things in the editor.

  2. River Martínez Avatar

    Hi Justin,

    Great tutorial, I was motivated to create my own “theme” everything went very well until creating new blocks in the “theme”, for example: create with “npx @wordpress/create-block my-custom-block –variant=dynamic –no-plugin” a block inside the “resource” directory but when compiling with “wp-scripts start –webpack-src-dir=resources –output-path=public” the whole structure of the block is not created, only the block.json and render.php files are copied into public directory. Any idea how to configure the “theme” to compile the files of the theme itself and the blocks? Thank you very much, greetings

    1. Justin Tadlock Avatar

      I’ve never added a block from a theme (and don’t generally recommend it), but you’d probably need to make sure that you’re importing const { getWebpackEntryPoints } = require( '@wordpress/scripts/utils/config' ) to get the core entry points and including them via ...getWebpackEntryPoints().

Leave a Reply

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