WordPress.org

WordPress Developer Blog

Per-block CSS with theme.json

WordPress 6.2 added support for per-block CSS in theme.json. That means now you can add in snippets of custom code when the built-in design tools don’t quite cover your use case.

theme.json supports enough CSS features to cover most common styles. But it cannot reasonably cover every scenario and still have a matching design tool in the editor UI, which is an important consideration. If the interface covered every possibility, the user experience would suffer.

There are times when you still need good ol’ CSS for block themes. 

Why not just stick CSS in style.css and be done with it? Why bother mixing CSS into theme.json?

The answers to those questions are up to you—you’re the theme designer. But the primary advantage of using theme.json over a stylesheet is that the .json file plugs the CSS back into the UI. And users can customize the code directly in the block’s custom CSS panel.

Modern WordPress lets themes and users communicate. The theme serves up a configuration via theme.json, and users can customize it in the interface. Under the hood, it’s all formatted into JSON—even the user customizations—and works under a standard set of rules. The more you buy into this system as a designer, the more flexibility your users will have.

There are a few pitfalls too, and you will learn about those. The system isn’t perfect, but the tools are solid enough that you can start digging into this feature and deciding when and where to use it.

Adding per-block CSS

You should already be familiar with how to customize block styles in theme.json. If not, get up to speed with the global settings and styles documentation.

You add custom styles by adding a new css property to a styles.blocks.[block-name] object in theme.json. And here’s something cool: when you add CSS in this way, you do not need to know the selector. WordPress will automatically generate that for the block.

Notice how this JSON code formats the css property:

{
	"css": "color: red;"
}

The value is a CSS declaration that probably looks familiar. The only difference is that the JSON wraps the declaration in quotes and assigns it to the css property.

Now, try your hand at adding a single CSS declaration. This theme.json code example adds a custom letter-spacing value to the Post Title block:

{
	"$schema": "https://schemas.wp.org/trunk/theme.json",
	"version": 2,
	"styles": {
		"blocks": {
			"core/post-title": {
				"css": "letter-spacing: 1px;"
			}
		}
	}
}

Save those changes to your theme.json. WordPress will generate the CSS for this on the front end and in the editor:

.wp-block-post-title {
	letter-spacing: 1px;
}

Now, open the Appearance > Editor screen in the WordPress admin. Then, open the Styles panel and select Blocks > Post Title > Additional block CSS. Your custom CSS should appear, as you see below:

Because you registered the CSS in theme.json, your users can make changes to it directly, right in the interface. They can skip the complexities of CSS specificity, and they’ll never know the hassle of trying to find the proper selectors with this example.

That’s a pretty powerful bridge between you and your users.

At the moment, WordPress only lets you add CSS to blocks in theme.json. There is an open ticket to add element support.

Tips, tricks, and pitfalls

You have learned how to add basic CSS declarations, but there are also a few tricks you can use to handle more complex scenarios.

Using the & selector and brackets

WordPress supports the & selector, much as you would see in a language like Sass. But it is not exactly the same. For example, it doesn’t support nested CSS blocks. 

But it does work for appending selectors to the WordPress-generated block class. 

A minute ago, you targeted the Post Title block, which has a class of  .wp-block-post-title. What if you wanted to target that only when it has a custom class attached to it? As, maybe, when you’re registering a custom block style variation? 

Suppose you registered a block style named letter-spacing-sm, which would have a generated class of .is-style-letter-spacing-sm. You could append that class with &.is-style-letter-spacing-sm and wrap your CSS declaration with brackets:

{
	"core/post-title": {
		"css": "&.is-style-letter-spacing-sm { letter-spacing: 1px; }"
	}
}

WordPress will generate this CSS:

.wp-block-post-title.is-style-letter-spacing-sm {
	letter-spacing: 1px;
}

Technically, you could write your custom code without the ampersand, and WordPress will append it correctly. But the & gives you a visual reminder that .is-style-letter-spacing-sm is being appended to another class.

Targeting a nested element

Where the ampersand really shines is with targeting nested elements. For example, suppose you wanted to rotate the text of Image block captions, as below:

If you wrote the code below, you would end up with a broken selector:

{
	"core/image": {
		"css": "figcaption { transform: rotate( 1deg ); }"
	}
}

WordPress would append figcaption to the block class name:

.wp-block-imagefigcaption {
	transform: rotate( 1deg );
}

You either have to put a space at the beginning of your code or use &. Remembering to add a space is not ideal, and it would be easier to break when users make edits.

It’s a lot easier and less prone to errors to write it like so:

{
	"core/image": {
		"css": "& figcaption { transform: rotate( 1deg ); }"
	}
}

Which will trigger WordPress to generate the expected CSS code block:

.wp-block-image figcaption {
	transform: rotate( 1deg );
}

Large CSS code blocks

If you’ve worked with JSON enough in the past, you’ve likely already noticed a couple of major drawbacks to saving CSS as JSON string values:

  • There’s no built-in syntax highlighting for the CSS.
  • JSON doesn’t support line breaks, so forget about using multi-line CSS blocks.

There might be some tooling to help you work around those limitations, but you will quickly run into issues when using the default system.

For example, you might want add a border that looks hand-drawn to the <img> element output by the Image block, as you see below:

This design takes several CSS declarations to pull off:

{
	"core/image": {
		"css": "& img { border: 2px solid currentColor; overflow: hidden; box-shadow: 0 4px 10px 0 rgba( 0, 0, 0, 0.3 ); border-radius: 255px 15px 225px 15px/15px 225px 15px 255px !important; }"
	}
}

And it’s the moment that you might start questioning whether using this feature is worth it, since JSON is not the ideal method for writing big blocks of CSS code. Add more than a few CSS declarations, and things get really messy, really fast.

The code also looks messy in the editor—hard for your users to read and a little scary for some to edit. One way around this: use \n for line breaks and \t for tabs in the final string. Here’s an example:

{
	"core/image": {
		"css": "& img {\n\tborder: 2px solid currentColor;\n\toverflow: hidden;\n\tbox-shadow: 0 4px 10px 0 rgba( 0, 0, 0, 0.3 );\n\tborder-radius: 255px 15px 225px 15px/15px 225px 15px 255px !important;\n}"
	}
}

That will give users a nicely-formatted CSS block in the interface:

& img {
	border: 2px solid currentColor;
	overflow: hidden;
	box-shadow: 0 4px 10px 0 rgba( 0, 0, 0, 0.3 );
	border-radius: 255px 15px 225px 15px/15px 225px 15px 255px !important;
}

The JSON might not look great to you, but your users will likely appreciate the CSS formatting.

Tip: use \" to escape double-quotes in your CSS when used in a JSON string.

When to use CSS in theme.json

CSS support in theme.json can be both a powerful feature and a recipe for headache. In its current state, it works well when you have small design changes for specific blocks, especially if your theme doesn’t load any stylesheets.

The integration with the Styles interface in the Site Editor is also great for users who want to customize these bits of CSS that your theme adds.

If you want to get the most out of it while also using bigger blocks of CSS, you want tools that soothe the pain points. For example, a script that pulls CSS from stylesheet files, formats the code as a valid JSON string, and saves it to theme.json could be the answer. That’s outside the scope of this post, but maybe it’ll motivate you to build solutions that push the boundaries of what theming could be.

Ultimately, it’s another tool in the toolbox. Use it when it makes sense for your project.

Props to @marybaum, @bph, and @poena for feedback and review. Photo of palm trees by @shameemreza from the WordPress photos directory.

12 responses to “Per-block CSS with theme.json”

  1. Alister Cameron Avatar

    Even for simple things – such as animations – hover states and such would be good to include.

  2. Dominique Pijnenburg Avatar

    Good ideas at the end of the article!

    I am still wondering if the user should always be able to access these kind of things, but at least having the ability to provide the access is a good thing.

  3. Eroan Avatar

    Really nice evolution, it will help so much in customizing default blocks for client projects 😍

  4. Eric Avatar
    Eric

    I have been playing with the experimental grid block and custom CSS and now would like to move the CSS into theme.json but cannot find how to use the custom CSS class (g1) that I added to the grid block on my home page with the site editor (I also assigned HTML Element: section.
    Under settings, I have:

    "gridCol-2": {
    "grid-template-columns": "repeat(2, minmax(0, 1fr))"
    }

    Under styles, I have:

    "css": "& is-section.g1 {display: grid; .is-style-gridCol-2!important;}"

    1. Justin Tadlock Avatar

      Is gridCol-2 like this in your theme.json?

      {
      "settings": {
      "custom": {
      "gridCol-2": {
      "grid-template-columns": "repeat(2, minmax(0, 1fr))"
      }
      }
      }
      }

      If so, you should be able to target the .g1 class when added to a block like so:

      "css": "&.g1 { display: grid; grid-template-columns: var( --wp--custom--grid-col-2--grid-template-columns ); }"

      Edit: Sorry for the bad formatting. We can’t post proper Code blocks here in the comments.

  5. earthstoriez Avatar

    Will it be possible to style the Drop Cap using this method?

  6. TS Avatar
    TS

    Are there some breaking changes to this system in 6.3?

    I just updated a local installation and custom css in theme.json is no longer applied in the frontend, just the editor.

    1. Justin Tadlock Avatar

      Not that I can see. I just ran a test on 6.3 with some custom CSS, and it worked for me.

      1. TS Avatar
        TS

        Thanks a lot for checking! Will have to debug this further then.

      2. TS Avatar
        TS

        I got some help in the site-editing slack!

        Apparently, this issue only affects hybrid themes: there’s an open issue on github –

        https://github.com/WordPress/gutenberg/issues/52644

        that also includes a workaround (that fixes it for me) for functions.php

        https://github.com/WordPress/gutenberg/issues/52644#issuecomment-1636776172

        1. apmeyer Avatar

          Thanks for this note and the link to the workaround. I was pulling my hair out trying to understand why it wasn’t working in a hybrid theme. Hopefully it’s addressed soon, it’s super handy.

  7. Divjot Singh Avatar

    Does this method support `:before` and `:after` pseudo classes?

Leave a Reply

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