WordPress.org

WordPress Developer Blog

Styling blocks: empowering users with CSS custom properties

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.json 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.

5 responses to “Styling blocks: empowering users with CSS custom properties”

  1. Mateus Machado Luna Avatar
    Mateus Machado Luna

    Hey Michael, thank you for the excellent and detailed post!

    I’ve been using color options on my blocks for a while, but this is the first time that I read about this `PanelColorSettings` component. I did some search and it seems that it is not documented yet: https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#panelcolorsettings.

    Could you tell me if there is any option on it that would allow us to add :hover and :focus state options to our custom blocks, just like the `core/paragraph` link options does?

    1. Michael Burridge Avatar

      Hi Mateus

      Sure, that’s do-able. Just follow all the same principles as outlined in the post. Suppose you want the question background to be a different colour on hover.

      Create a new attribute:

      "questionHoverBgColor": {
      "type": "string",
      "default": "#aaaaaa"
      }

      Add a new element to the colorSettings array in the PanelColorSettings component:

      {
      value: questionHoverBgColor,
      onChange: onChangeQuestionHoverBgColor,
      label: __( 'Question hover background color' ),
      }

      Create the onChange function:

      const onChangeQuestionHoverBgColor = ( val ) => {
      setAttributes( { questionHoverBgColor: val } );
      };

      Add the custom property to the styles object:

      "--question-hover-bg-color": questionHoverBgColor

      Finally, reference the CSS custom property in the appropriate selector in the CSS:

      .wp-block-create-block-css-demo summary:hover {
      background-color: var(--question-hover-bg-color);
      }

      Hope this helps. Let me know if anything is unclear or you need further help with this.

      Thanks for the heads-up re the missing documentation.

      1. Mateus Machado Luna Avatar
        Mateus Machado Luna

        Thanks again for the explanation!

        That will do it, but just to be perfectionist… it won’t actually add the :hover color option as a color “related” to the Question color, right? I was looking for an option where could have something like this:

        https://ibb.co/9bzZ28t

        Is it possible with this component or is it a completely different thing?

        1. Michael Burridge Avatar

          Yes, you can create a second PanelColorSettings component just for the question settings, and separate from the one for the header settings. See: https://ibb.co/hZqjgR3

          Then you’ll get something like this in the editor: https://ibb.co/Cw24M0c

          Hope this helps.

          1. Mateus Machado Luna Avatar
            Mateus Machado Luna

            You are right, that is the way to go. I was hopping we could do something that is a bit more advanced, but after exploring a bit, I understood why it is not.

            My expectations were that the `PanelColorSettings` could create inputs in the same way that the `ColorPanel` from the `GlobalStyles` package does: https://github.com/WordPress/gutenberg/blob/trunk/packages/block-editor/src/components/global-styles/color-panel.js

            Here I can see that if we pass a multivalued array to that `indicators` and `tabs`, the color option would be rendered as a tabbed option inside the the color option (which is why the Paragraph block shows this options as in the screenshot). The problem is that this is only passed as a multivalued array for the link property:
            https://github.com/WordPress/gutenberg/blob/fffaab7678c960e86eb745d281bb044a6ab4923e/packages/block-editor/src/components/global-styles/color-panel.js#L558

            So custom properties that I pass later (if I understood well) are added here, always as an array of one single element:
            https://github.com/WordPress/gutenberg/blob/fffaab7678c960e86eb745d281bb044a6ab4923e/packages/block-editor/src/components/global-styles/color-panel.js#L654C6

            Anyway, that was a long trip! We can achieve the same result adding new elements as you suggested. I do prefer the “stacked” view that the tabs add, but I suppose the component is not ready to add this for any property that we create.

Leave a Reply

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