The Block Supports
API allows you to easily add a variety of options to your block, including style settings such as color
and spacing
. You can do this with a simple definition in block.json
:
{
.
.
"supports": {
"color": {},
"spacing": {
"margin": true,
"padding": true
}
}
}
When you add these “supports” properties to your block.jso
n file your users get controls in the Settings sidebar that allow them to change these settings.
However, these block supports
settings only apply to the root level of the block – they only apply to the wrapping element. This is fine for simple blocks with a single element, such as a single paragraph (<p>...</p>
) or heading (<h2>...</h2>
).
Note: WordPress 6.3 now includes the Selectors API which enables users to apply block supports settings to child elements of your block, so it’s no longer strictly true that block supports only apply to the wrapping element.
But what if your block has more complex markup and consists of a hierarchy of HTML elements?
<div class="wp-block-css-demo">
<header>
<h2>Mountains</h2>
<p>We answer all your most frequently asked questions about mountains</p>
</header>
<section>
<details>
<summary>What is the highest mountain in the world?</summary>
<div class="answer">
Mount Everest, in the Himalayas, stands at 8849 meters which makes it the highest mountain in the world.
</div>
</details>
<details>
<summary>Which is the highest free-standing mountain?</summary>
<div class="answer">
Kilimanjaro, a dormant volcano in Tanzania, is the world's highest free-standing mountain at 5895 meters.
</div>
</details>
<details>
<summary>What is the farthest point from the centre of the Earth?</summary>
<div class="answer">
The peak of Mount Chimborazo in Equador is the furthest point from Earth's centre. The summit is 2072 meters farther from Earth's centre than Mount Everest's summit.
</div>
</details>
</section>
</div>
In addition, what if you want to give your users the ability to control the styling of child elements within this complex block? You may want to give your users the option to style the <header>
element, or the options to style separately the <summary> and <div.answer>
elements within the <details>
elements.
Ideally, the user should be able to style every child element within the block individually so that they have maximum control over the look and feel of the block on their site.
Block supports cannot do what we need here. Another solution needs to be sought. This solution may not be immediately obvious.
Why complex markup in a block?
Now you may be asking, why create a block like that? A complex block can be created from more atomic blocks grouped together within a Group block, or blocks can be composed into a pattern.
One answer to that is that not everything you might want to put into a group or pattern exists as a block. Take the example above, while a details/summary
block exists in the Gutenberg plugin it doesn’t, at the time of writing, exist in WordPress core yet. The details/summary
block is slated for inclusion in WordPress 6.3 but the styling options will be limited. For example, you cannot separately style the <summary>
element from the rest of the block.
Another reason that you might have complex markup in your block could be because the content is being dynamically generated. For example, the FAQs in each of the <details>
elements in the example above could come from a Custom Post Type (CPT) rather than being hard-coded into the markup as illustrated.
CSS Custom Properties
So a block may well have good and legitimate reasons to consist of complex markup. The way to give users control over the styling of child elements within a block with complex markup involves using CSS custom properties (which are also sometimes called CSS variables).
You may be accustomed to using CSS custom properties on the root element of your stylesheet:
:root {
--primary-color: midnightblue;
--secondary-color: seashell;
--button-border-radius: 12px;
}
This makes a great deal of sense as the values can then be accessed by all the elements in the DOM.
However, it’s not necessary to define your CSS custom properties at the root level. CSS custom properties can be defined on a particular element or selector:
.wp-block-css-demo {
--header-heading-color: #FFF555;
--header-bg-color: #537FE8;
}
In that case they are scoped to that element and the values are then available to that element and to child elements of that element. However, the values are not available to any other elements in the DOM.
.wp-block-css-demo header {
background: var(--header-bg-color);
}
.wp-block-css-demo header h2 {
color: var(--header-heading-color);
}
So given that CSS custom properties can be defined on an element in a stylesheet, it follows that they can be inlined on that element:
<div class="wp-block-css-demo" style="--header-heading-color: #FFF555; --header-bg-color: #537FE8;">
.
.
</div>
Importantly, these inlined CSS custom properties can still be referenced in the stylesheet file in exactly the same way.
Inlining CSS custom properties on the block’s wrapper element is how you can give users control over the styling of child elements in your block.
Let’s see how this can be done.
The Edit()
component
Let’s suppose, for this example, that you want to give users the ability to change the background color of the <header>
element and the text color of the <h2>
element within the <header>
element.
Although color properties are being used in this example the principles demonstrated here will apply to any CSS property, e.g. border properties such as width and radius, spacing properties such as margin and padding, and even transforms such as rotate and scale.
Let’s suppose also that the markup above is what is generated by your block. Your Edit()
component might look something like this:
export default function Edit() {
return (
<div { ...useBlockProps() }>
<header>
<h2>Mountains</h2>
<p>We answer all your most frequently asked questions about mountains</p>
</header>
<section>
<details>
<summary>What is the highest mountain in the world?</summary>
<div class="answer">
Mount Everest, in the Himalayas, stands at 8849 meters which makes it the highest mountain in the world.
</div>
</details>
<details>
<summary>Which is the highest free-standing mountain?</summary>
<div class="answer">
Kilimanjaro, a dormant volcano in Tanzania, is the world's highest free-standing mountain at 5895 meters.
</div>
</details>
<details>
<summary>What is the farthest point from the centre of the Earth?</summary>
<div class="answer">
The peak of Mount Chimborazo in Equador is the furthest point from Earth's centre. The summit is 2072 meters farther from Earth's centre than Mount Everest's summit.
</div>
</details>
</section>
</div>
);
}
In reality each of the FAQs would probably come from a custom post type and be rendered programmatically, but this hard-coded markup is fine to illustrate the principle.
First up, you’re going to need a couple of attributes in your block.json
file:
{
.
.
"attributes": {
"headerBackgroundColor": {
"type": "string",
"default": "#537FE8"
},
"headerHeadingColor": {
"type": "string",
"default": "#FFF555"
}
}
}
You can optionally provide some default values. This is, in fact, a good idea so that there’s some default styling even before the user changes things, or if the user opts not to change anything.
You can get the attributes into your Edit()
component by destructuring them from the object passed to the component:
export default function Edit( { attributes } ) {
.
.
}
Now create an object. The properties in that object will have the names of the CSS custom properties that you want to use, and values taken from the corresponding attributes:
const styles = {
"--header-bg-color": attributes.headerBackgroundColor,
"--header-heading-color": attributes.headerHeadingColor,
};
Note: see the section below on theme compatibility for guidance on choosing suitable names for your CSS custom properties.
And now for the magic 🪄. Pass the object you just created as the value to a style
property in an object which in turn is passed to the useBlockProps
hook on the block’s wrapper element:
<div { ...useBlockProps( { style: styles } ) }>
.
.
</div>
Along with classes and block supports attributes, useBlockProps
will spread the contents of the object passed to it onto the wrapping element when it’s rendered in the editor.
So the wrapping <div>
will get a style
attribute with inline styles, and the inline styles that form the contents of that attribute will be the two CSS Custom Properties with the values received from the block attributes:
These inlined CSS custom properties can then be referenced from the block’s stylesheet (note that this is SCSS which needs a compile step):
.wp-block-create-block-css-demo {
.
.
& header {
background-color: var(--header-bg-color);
}
& header h2 {
color: var(--header-heading-color);
}
}
Adding controls
Now we need to give the user some controls so that they can change the values stored in the attributes. The controls you provide depends on the kind of properties you want the user to be able to change, but as colors are being used in this example a <PanelColorSettings>
component is what you need here.
First import PanelColorSettings
and InspectorControls
from @wordpress/block-editor
, and then add the <PanelColorSettings>
component within the <InspectorControls>
component in the JSX returned by the Edit()
component:
<InspectorControls>
<PanelColorSettings
title={ __( 'Header settings' ) }
colorSettings={ [
{
value: attributes.headerBackgroundColor,
onChange: onChangeHeaderBackgroundColor,
label: __( Background color ' ),
},
{
value: attributes.headerHeadingColor,
onChange: onChangeHeaderHeadingColor,
label: __( 'Heading color' ),
},
] }
/>
</InspectorControls>
Then whenever the block is re-rendered it will pick up the colors that the user has defined in the attributes using these controls, and those values will be assigned to the CSS custom properties which are inlined onto the block’s wrapping element.
Remembering to destructure setAttributes
and to create the onChange
functions, your Edit()
component should look like this:
import { __ } from '@wordpress/i18n';
import {
useBlockProps,
InspectorControls,
PanelColorSettings,
} from '@wordpress/block-editor';
import './editor.scss';
export default function Edit( { attributes, setAttributes } ) {
// destructure the attributes
const {
headerBackgroundColor,
headerHeadingColor
} = attributes;
// define the styles object
const styles = {
"--header-bg-color": headerBackgroundColor,
"--header-heading-color": headerHeadingColor,
};
// define the onChange functions
const onChangeHeaderBackgroundColor = ( val ) => {
setAttributes( { headerBackgroundColor: val } );
};
const onChangeHeaderHeadingColor = ( val ) => {
setAttributes( { headerHeadingColor: val } );
};
return (
<>
<InspectorControls>
<PanelColorSettings
title={ __( 'Header settings' ) }
colorSettings={ [
{
value: headerBackgroundColor,
onChange: onChangeHeaderBackgroundColor,
label: __( 'Background color' ),
},
{
value: headerHeadingColor,
onChange: onChangeHeaderHeadingColor,
label: __( 'Heading color' ),
},
] }
/>
</InspectorControls>
<div { ...useBlockProps( { style: styles } ) }>
<header>
<h2>Mountains</h2>
<p>We answer all your most frequently asked questions about mountains</p>
</header>
<section>
<details>
<summary>What is the highest mountain in the world?</summary>
<div class="answer">
Mount Everest, in the Himalayas, stands at 8849 meters which makes it the highest mountain in the world.
</div>
</details>
<details>
<summary>Which is the highest free-standing mountain?</summary>
<div class="answer">
Kilimanjaro, a dormant volcano in Tanzania, is the world's highest free-standing mountain at 5895 meters.
</div>
</details>
<details>
<summary>What is the farthest point from the centre of the Earth?</summary>
<div class="answer">
The peak of Mount Chimborazo in Equador is the furthest point from Earth's centre. The summit is 2072 meters farther from Earth's centre than Mount Everest's summit.
</div>
</details>
</section>
</div>
</>
);
}
The save()
function
If you’re creating a static block then do exactly the same in your save()
function – apart, of course, from the <InspectorControls>
controls and onChange
functions. Your save()
function should look like this (note that some of the markup has been removed for brevity):
import { useBlockProps } from "@wordpress/block-editor";
export default function save( { attributes } ) {
// destructure the attributes
const {
headerBackgroundColor,
headerHeadingColor
} = attributes;
// define the styles object
const styles = {
"--header-bg-color": headerBackgroundColor,
"--header-heading-color": headerHeadingColor,
};
return (
<>
<div {...useBlockProps.save( { style: styles } ) }>
<header>
// header content
</header>
<section>
// section content - <details> elements go here
</section>
</div>
</>
);
}
Dynamic blocks
If you’re creating a dynamic block then things are kind of the same, but also kind of different. Let’s dig in and see how to achieve the same thing, namely add a style
attribute with our CSS custom properties as its value to the wrapper element, but in a dynamic block.
For a dynamic block the Edit()
component that gets rendered in the editor is exactly the same as outlined above. But a dynamic block does not have a save()
function as it is server-side rendered. Instead it either has a render()
function or, more recently, a render file defined in a render
property in block.json
:
{
.
.
render: "file:./render.php",
.
.
}
Doing it in this more modern way means you don’t need to pass the attributes to a function. The render file auto-magically receives the attributes in an associative array as $attributes
.
Recall that earlier we passed our styles to the useBlockProps
hook in an object, which useBlockProps
spread onto the wrapper element. A similar principle applies In the PHP for a dynamic block, but rather than an object we instead pass a string to get_block_wrapper_attributes
which needs to be echo
-ed onto the wrapper element.
We can construct our string like this:
$styles = "--header-bg-color: " . $attributes[ "headerBackgroundColor" ] . ";";
$styles .= "--header-heading-color: " . $attributes[ "headerHeadingColor" ];
And then use it as the value for a style
property in an associative array that gets passed to get_block_wrapper_attributes
:
<div <?php echo get_block_wrapper_attributes( array( "style" => $styles ) ); ?>>
.
.
</div>
So the principle is the same, and the process is analogous, but there are crucial differences that you should be aware of when creating a dynamic block.
Your render.php
file should look like this:
<?php
$styles = "--header-bg-color: " . $attributes[ "headerBackgroundColor" ] . ";";
$styles .= "--header-heading-color: " . $attributes[ "headerHeadingColor" ];
?>
<div <?php echo get_block_wrapper_attributes( array( "style" => $styles ) ); ?>>
<header>
<h2>Mountains</h2>
<p>
We answer all your most frequently asked questions about mountains
</p>
</header>
<section>
<details>
<summary>What is the highest mountain in the world?</summary>
<div class="answer">
Mount Everest, in the Himalayas, stands at 8849 meters which makes it the highest mountain in the world.
</div>
</details>
<details>
<summary>Which is the highest free-standing mountain?</summary>
<div class="answer">
Kilimanjaro, a dormant volcano in Tanzania, is the world's highest free-standing mountain at 5895 meters.
</div>
</details>
<details>
<summary>
What is the farthest point from the centre of the Earth?
</summary>
<div class="answer">
The peak of Mount Chimborazo in Equador is the furthest point from Earth's centre. The summit is 2072 meters farther from Earth's centre than Mount Everest's summit.
</div>
</details>
</section>
</div>
When this code is rendered in the front end it uses exactly the same CSS file that references the CSS custom properties which are now inlined on the wrapper element, so the content still looks exactly the same as it did with the static block that we looked at earlier.
A note about theme compatibility
Earlier you set default values for the attributes. But you should also bear in mind that theme authors might also want to set default values in their theme’s theme.json
file.
To enable theme authors to set default values for the child elements in your complex block some consideration should be given to the naming of the CSS custom properties.
A specific format should be followed. This is:
--wp--custom--{namespace}--{attributeName}
Each part of the name is separated by a double dash --
. So, assuming that our namespace is going to be css-demo
, the format for the names of the two CSS custom properties used earlier should be:
--wp--custom--css-demo--header-bg-color
--wp--custom--css-demo--header-heading-color
By specifying your CSS custom property names in this way theme authors can provide default values by using the settings.custom.cssDemo.headerBgColor
and settings.custom.cssDemo.headerHeadingColor
properties in their theme.json
files.
Props to @webcommsat, @bph, @greenshady for reviewing this post.
Leave a Reply