I think I’m getting lazy. And I don’t think the WordPress Global Styles system is helping. I can just throw a few values in theme.json
or a variation’s JSON file, and they’re automatically scoped, only targeting the necessary elements or blocks in the generated CSS. It’s a pretty neat system.
We’re very much in the early days of this era of block theming, which will be a long one. And I still, certainly, write my fair share of CSS—WordPress doesn’t do all the things just yet. But with each new major WordPress release, I find myself writing less and less actual CSS and using JSON to handle styles.
Personally, I’d rather just write CSS. Mostly, because I have spent years crafting solid styling systems. I can use and reuse to get what I want. But also because I’m a developer. And that’s what developers do.
But this isn’t about me. It’s about the millions of people who use WordPress and don’t know a bit of code. It’s about empowering them to style their own sites, taking us from democratizing publishing to democratizing design.
The real winners of the Global Styles Engine in WordPress are the users. So it doesn’t matter as much where I think styles should live as it does making a user’s life easier.
In this article, let’s go over some tricks and shortcuts I use to build block themes—you might think of it as a sort of an in-case-you-didn’t-know-it post.
I’ll also tell you when you absolutely have to fall back on that ol’ reliable CSS.
Table of Contents
Leveraging JSON to add global settings and styles
The on-ramp to styling themes is the Global Styles system. You’ll often see this called theme.json
. In the early days of the Block Editor, theme.json
was the only place you could add style rules with JSON, so that became the shorthand for the engine itself. But today, things have changed.
Now, just weeks ahead of WordPress 6.7, styling can go into several types of JSON files, and we’ll dive into each of those below.
For more information on how this system works, refer to the Global Settings and Styles documentation in the Theme Handbook.
A quick theme.json overview
The primary styles for a WordPress theme should live in a file named theme.json
in the theme’s root folder. Think of this as the theme’s foundational set of styles. Whenever a situation calls for a more specific set of styles (e.g., user styles, block style variations), they are merged with and outrank those in theme.json
.
The theme.json
file in your theme can be broken down into these top-level keys:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {},
"styles": {},
"customTemplates": {},
"templateParts": {},
"patterns": []
}
For the purposes of this tutorial, we’ll focus on two of these:
settings
: An object for defining what design tools are available to the user in the UI, but you can also use it to add presets (i.e., CSS custom properties).styles
: An object for defining root, element, and block styles, with each option mapping to CSS properties.
You probably know the basics of using theme.json
, so let’s get right to adding some of that power and finesse that doesn’t need custom CSS.
Fun with style variations
Right now, the Block Editor will let you use four types of style variations in your theme:
- Theme (or “Global”): Can overwrite every aspect of the
theme.json
file, including settings, styles, and other top-level keys. - Color: Can overwrite color settings or styles.
- Typography: Can overwrite typography settings or styles.
- Block: Can overwrite styles for a specific block and its nested blocks and elements.
You must place any custom style variation JSON file under your theme’s /styles
folder. There is no required naming or organizational convention, but I recommend a nested folder structure that looks similar to this:
/styles
/block
/variation-filename.json
/color
/variation-filename.json
/theme
/variation-filename.json
/typography
/variation-filename.json
Theme style variations
Theme—often called “Global”—variations have been the most common type of style variations for themes. Essentially, they are alternative versions of the primary theme.json
file and can do everything theme.json
can do.
Here’s a page in the Site Editor that uses the default theme.json
file (no variation selected):
Now, here’s the same page, but with the Saga variation selected from under the Styles panel in the Site Editor. You’ll notice that its styles are almost entirely different from the default style. It has different typography, colors, and border styles, for example:
That’s the power of theme style variations. You can define a wide array of curated styles to give users a choice of what their sites can look like, and they can make those choices in as little as one click. Plus, when you combine theme variations with color and typography variations, you give your users a multitude of options they can apply faster than ever. (More on that below.)
To ensure WordPress will recognize a JSON file as a theme style variation, it needs to respect these rules:
- Place it in the
/styles
folder (recommended under/styles/theme
). - If it has only color or only typography styles, it becomes a color or typography style variation. So you need to add some other settings or styles to the file—just about anything will work.
Other than that, pretty much anything goes. Anything defined in your theme style variation’s file will overrule the same definition in your theme.json
.
For example, here is a small sampling of what a theme style variation might look like that adds a background color, text color, and defines heading-element typography:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"title": "Example",
"styles": {
"color": {
"background": "var:preset|color|white",
"text": "var:preset|color|black"
},
"elements": {
"heading": {
"typography": {
"fontWeight": "700"
}
}
}
}
}
Check out Introduction to theme.json to learn more about all the settings and styles that you can define.
The var:preset|$feature|$slug
syntax in the above code is the primary way to apply presets as styles. Alternatively, you can also use CSS directly and reference var(--wp--preset--$feature--$slug)
.
Color style variations
Added in WordPress 6.6, color style variations can be very powerful. Essentially, the feature lets you define curated color palette presets for users to choose from, giving them quick and easy options they can feel sure will work in the theme.
You can find these variations in the Styles panel in the Site Editor. Below, the Evenfall color style variation is active:
To make sure your color style variation shows up where your user will see it in the UI, place the JSON file with your color styles in the /styles
folder:
- It may only have color settings or styles.
- Best practice: put it under
/styles/color
.
Want to make your life easier? Use the very same keys you define in your primary theme.json
to define your settings.color
properties — especially palette
, duotone
, and gradients
— in every other JSON file you write. You’ll develop muscle memory when you’re writing themes, and you’ll always know where your colors live, even in a theme you haven’t touched in five years.
With that guideline mind, here is what a custom color variation could look like (of course, please use slugs that match your theme’s color slugs):
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"title": "Evenfall",
"settings": {
"color": {
"palette": [
{
"slug": "primary-900",
"color": "#6e2057",
"name": "Primary: 900"
},
{
"slug": "primary-700",
"color": "#b93792",
"name": "Primary: 700"
},
{
"slug": "primary-500",
"color": "#d163b0",
"name": "Primary: 500"
},
{
"slug": "primary-300",
"color": "#e9b4d9",
"name": "Primary: 300"
},
{
"slug": "primary-100",
"color": "#f9ebf5",
"name": "Primary: 100"
},
{
"slug": "black",
"color": "#000000",
"name": "Black"
},
{
"slug": "neutral-900",
"color": "#1e1b27",
"name": "Neutral: 900"
},
{
"slug": "neutral-700",
"color": "#5c5379",
"name": "Neutral: 700"
},
{
"slug": "neutral-500",
"color": "#a29aba",
"name": "Neutral: 500"
},
{
"slug": "neutral-300",
"color": "#c7c2d6",
"name": "Neutral: 300"
},
{
"slug": "neutral-100",
"color": "#ebeaf1",
"name": "Neutral: 100"
},
{
"slug": "white",
"color": "#ffffff",
"name": "White"
}
]
}
}
}
And this, my friend, is how color style variations work flawlessly no matter what other variations they get paired with.
Typography style variations
Typography style variations are also new in WordPress 6.6. As with colors, the feature gives you the power to add alternate typesets that work well for your theme—and show them to your users as a range of curated presets they can choose in a single click.
Typography variations show up in the Styles panel of the Site Editor. As with colors, they have their own panel. Below, the Fairy Tale variation is selected:
Just like color variations, there are a couple of rules to follow for WordPress to recognize a JSON file as a typography style variation:
- Place it in the
/styles
folder (recommended under/styles/typography
). - It can only have typography settings or styles.
Pro tip: When defining font families via settings.typography.fontFamilies
, use a consistent naming convention for your slugs across all variations (e.g., primary
, secondary
, etc.). This will ensure that referenced families are defined across variations.
This snippet shows how to define a custom typography style variation:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"title": "Fairy Tale",
"settings": {
"typography": {
"fontFamilies": [
{
"fontFamily": "'Elsie Swash Caps', system-ui",
"slug": "primary",
"name": "Primary (Elsie Swash Caps)",
"fontFace": [
{
"fontFamily": "Elsie Swash Caps",
"fontWeight": "400",
"fontStyle": "normal",
"fontStretch": "normal",
"src": [ "file:./public/fonts/elsie/elsie-swash-caps.woff2" ]
},
{
"fontFamily": "Elsie Swash Caps",
"fontWeight": "900",
"fontStyle": "normal",
"fontStretch": "normal",
"src": [ "file:./public/fonts/elsie/elsie-swash-caps-900.woff2" ]
}
]
},
{
"fontFamily": "Lora, Rockwell, 'Rockwell Nova', 'Roboto Slab', 'DejaVu Serif', 'Sitka Small', serif",
"slug": "secondary",
"name": "Secondary (Lora)",
"fontFace": [
{
"fontFamily": "Lora",
"fontWeight": "400 700",
"fontStyle": "normal",
"fontStretch": "normal",
"src": [
"file:./public/fonts/lora/lora.woff2"
]
},
{
"fontFamily": "Lora",
"fontWeight": "400 700",
"fontStyle": "italic",
"fontStretch": "normal",
"src": [
"file:./public/fonts/lora/lora-italic.woff2"
]
}
]
}
]
}
},
"styles": {
"elements": {
"button": {
"typography": {
"fontFamily": "var:preset|font-family|secondary"
}
},
"caption": {
"typography": {
"fontFamily": "var:preset|font-family|primary",
"fontSize": "var:preset|font-size|sm",
"fontStyle": "normal",
"textTransform": "none"
}
},
"heading": {
"typography": {
"fontWeight": "400",
"textTransform": "none"
}
}
},
"blocks": {
"core/site-title": {
"typography": {
"fontFamily": "var:preset|font-family|primary",
"fontSize": "var:preset|font-size|lg",
"fontStyle": "normal",
"fontWeight": "900",
"letterSpacing": "0",
"textTransform": "none"
}
}
}
}
}
Now, notice that for this variation I defined more than just typography presets under settings.typography
—I included custom styles for elements and blocks under the styles
property. And that’s not at all what I had done for colors.
The reason: in my experience, typography almost always needs fine-tuning for everywhere it appears. That’s a lot of block- and element-specific places.
Block style variations and section styles
Block style variations have been around for a while. But before 6.6, you had to write PHP and load a custom stylesheet.
Now, though? You can define your block style variations, and your section styles, with JSON files, like a civilized human being.
The idea was to let you build section styles, so you could style Groups (and other blocks people like to use as containers) with custom color schemes. But you can do so much more than just that one little use case. You can define JSON-registered block styles for any block you want.
Section styles
Take a look below. Here’s the Style 3 block style variation applied to a Group block, giving it a dark blue background and white text:
Again, there are a couple of rules to follow for WordPress to recognize a JSON file as a block style variation:
- Put it in the
/styles
folder (recommended under/styles/block
). - House the blocks that define the variation in a
blockTypes
array.
Here is a basic example of a block style named section-1
that defines the background and text color:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"title": "Style 1",
"slug": "section-1",
"blockTypes": [
"core/column",
"core/columns",
"core/group"
],
"styles": {
"color": {
"background": "var:custom|color|primary-700",
"text": "var:custom|color|text|white"
}
}
}
Block style variations don’t have any particular rules about what you can style like typography and color variations. The main difference is that the styles are applied specifically to the block and any nested blocks or elements.
NOTE: The community is starting to settle on some standard naming conventions across this new world of interoperable themes and variations.
The Twenty Twenty-Five theme will start off with the convention for section styles, but you can start using it now.
Name your slug with the prefix section-
, then add a number. That’s it—no other values, at least in the slug. When you’re done, you’ll have a logical list of section styles whose slugs are section-1
, section-2
, and so on.
Text styles
Another convention coming out of the work on Twenty Twenty-Five: block style variations for purely textual elements. The goal there is to force (I mean, gently encourage, to save them hours down the road!) your users to define and style common textual elements at the beginning of their work.
Here’s a kicker. Really: a kicker is a short, small headline that sits above a larger headline:
Text styles work like any block styles, but the convention is leaning to applying them to blocks like Heading and Paragraph—blocks a user will use over and over again, and that should always look the same in context.
Per that emerging convention, the slug should have the prefix text-
and then a unique identifier like text-display
or text-subtitle
— or text-kicker
.
Here’s the Kicker style above. To use it, I recommend placing it in your theme’s /styles/block
folder with the filename of text-kicker.json
:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"title": "Kicker",
"description": "A short, one-line heading meant to be placed above a larger heading.",
"slug": "text-kicker",
"blockTypes": [
"core/heading",
"core/paragraph"
],
"styles": {
"typography": {
"fontFamily": "var:preset|font-family|secondary",
"fontSize": "var:preset|font-size|xs",
"fontWeight": "600",
"lineHeight": "var:custom|line-height|xs",
"textTransform": "uppercase"
}
}
}
Turn up the power with custom presets
theme.json
gives you superpowers most people never use: the ability to define custom presets, that let you define CSS custom properties using a standard convention.
And get this: you can override them in theme style variations.
You can define as many or as few custom presets as you want via the settings.custom
property:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"custom": {}
}
}
Note that color, typography, and block style variations do not currently support custom presets. You can only define them in theme.json
or a theme style variation.
Using custom presets in JSON
Let’s look at a practical example. Suppose you had a common border style you wanted to apply to many different blocks.
For example, here’s what such a global border style would look like on the Calendar block:
Now, you do not want to apply those same styles and update every block every time you decide to change them. After about three instances, that’s a bug-filled pain, just waiting to hit the back of your head.
In CSS, you’d typically define something like a --global--border
custom property to use throughout your design. And anytime you updated the property, it’d automatically apply wherever you used it.
You can do the very same thing with theme.json
. So let’s define a global border style and apply it to the blocks we want it on (in this case, the Calendar block, but feel free to use it across any blocks that make sense):
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"custom": {
"global": {
"border": {
"color": "var:preset|color|primary-100",
"style": "solid",
"width": "1px",
"radius": "6px"
}
}
}
},
"styles": {
"blocks": {
"core/calendar": {
"border": {
"color": "var:custom|global|border|color",
"style": "var:custom|global|border|style",
"width": "var:custom|global|border|width",
"radius": "var:custom|global|border|radius"
}
}
}
}
}
You’ll probably notice the var:custom
syntax. That’s different from the var:preset
syntax that you see with standard presets. You’ll change it for the generated CSS custom properties (--wp-custom--
vs. --wp--preset--
), as well.
Check out the Using Presets documentation for more information on how standard and custom presets work.
Now, here’s the big win with custom presets: they update across your theme style variations. Instead of styling the border for every block that uses the global border style, you define the border in one place, and that styling updates every time you change it. Automatically.
Suppose you had a theme style variation where you only wanted to change the radius for the global border to 0
(no rounding of the corners). You’d only need to overwrite that specific property:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"title": "Memories",
"settings": {
"custom": {
"global": {
"border": {
"radius": "0"
}
}
}
}
}
Then, it will automatically apply to any blocks using the global border presets, as you see below:
Mixing custom presets with stylesheets
Sometimes you still need to combine the CSS in your theme’s stylesheet(s) with custom presets. For example, all too often, I need to adjust line heights when a user has chosen a specific font size.
Because WordPress currently has no way to tie these two together with the standard properties in theme.json
, it’s up to you to find a good fix.
So, let’s define custom small, medium, and large line heights and font sizes in theme.json
:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"custom": {
"lineHeight": {
"sm": "1.625",
"md": "1.6875",
"lg": "1.5"
}
},
"typography": {
"fontSizes": [
{
"name": "Small - Fluid",
"size": "clamp(0.9444rem, 0.3268cqi + 0.8464rem, 1.0556rem)",
"slug": "sm"
},
{
"name": "Medium - Fluid",
"size": "clamp(1.06rem, 0.37cqi + 0.95rem, 1.19rem)",
"slug": "md"
},
{
"name": "Large - Fluid",
"size": "clamp(1.2rem, 0.85cqi + 0.94rem, 1.48rem)",
"slug": "lg"
}
]
}
}
}
When a user selects one of those font sizes in the UI, you need a way to apply the custom line height based on the chosen size. Fortunately, WordPress adds slug-based classes to the block in this case in the form of .has-{slug}-font-size
.
To define a line height based on the font size, you can add this to your theme’s style.css
file:
.has-sm-font-size { line-height: var(--wp--custom--line-height--sm); }
.has-md-font-size { line-height: var(--wp--custom--line-height--md); }
.has-lg-font-size { line-height: var(--wp--custom--line-height--lg); }
Take note of the naming scheme that WordPress generates for each of the line-height presets: --wp--custom--line-height--{slug}
.
Sometimes you still need CSS
You can’t design everything with JSON properties. That’s simply the reality of building a standardized style engine on top of CSS. It will never cover every possible CSS feature you might need.
And that’s OK. You can still write CSS to supplement what you’ve built with JSON.
Let’s look at two common scenarios where you might use custom CSS for block style variations:
- CSS in JSON: Use when you have a tiny bit of custom CSS.
- CSS in stylesheets: Use when you have more than a few lines of CSS.
Using CSS in JSON
Every now and again, you’ll need to add just a few lines of custom CSS in theme.json
or a style variation—few enough that it doesn’t really make sense to put them in a separate stylesheet.
For example, take a look at the first line (it’s in bold) of the first paragraph in this screenshot:
This is a Lead-in text style, similar to the text-based block style variations you and I walked through a few minutes ago. To style the first line of a Paragraph block in CSS, you must use the ::first-line
pseudo-selector. That’s not a standard value that you can set in the WordPress style engine.
Typically, you’d write this out in a stylesheet:
p.is-style-lead-in::first-line{
font-weight: 700;
font-synthesis: none;
font-variant: small-caps;
}
But it’s such a small amount of code, and WordPress lets you add custom CSS via the styles.css
property in your variation JSON file.
The downsides to writing CSS in JSON: you lose syntax highlighting, and you have to write all the code on one line. (That gets hard to read!) My rule of thumb: three lines of CSS in JSON is acceptable; anything more goes in a stylesheet.
To create this style, add a text-lead-in.json
file in your theme’s /styles/block
directory with the following code:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"title": "Lead-in",
"description": "Sets the first line in contrasting type preceding a headline or text.",
"slug": "text-lead-in",
"blockTypes": [
"core/heading",
"core/paragraph"
],
"styles": {
"css": "&::first-line{\n\tfont-weight: 700;\n\tfont-synthesis: none;\n\tfont-variant: small-caps;\n}"
}
}
The upside to using this method: the CSS will show up in the Site Editor, where your users can customize it.
Notice that I used \n
(new line) and \t
(tab) characters for the css
property value in the code. This is to format it so that it doesn’t come out as a single line in the Additional CSS box in the Site Editor. This makes it easier to customize for users.
Using CSS in stylesheets
Sometimes the design you have in mind gives you no choice: you have to put the CSS into a stylesheet to bring it to life. For me, that usually happens when I’m building really novel block style variations.
For example, the below is a Pullquote style named Mark: Top that puts a double-quote mark above the quotation:
That is very hard to do with theme.json
. To create this type of block style variation, you must first register it with PHP:
add_action( 'init', 'themeslug_register_block_styles' );
function themeslug_register_block_styles() {
register_block_style( 'core/pullquote', [
'name' => 'mark-top',
'label' => __( 'Mark: Top', 'themeslug' )
]);
}
Then, add the custom CSS for the design either in your theme’s primary style.css
file or, preferably, a block stylesheet:
.wp-block-pullquote.is-style-mark-top {
padding: 0;
border: none;
}
.wp-block-pullquote.is-style-mark-top blockquote {
padding: 1em 0 0;
display: flex;
flex-direction: column;
}
.wp-block-pullquote.is-style-mark-top blockquote::before {
content: open-quote;
color: currentColor;
font-size: 4em;
line-height: 0.25;
font-weight: 700;
}
More to explore
This tutorial idea began with a title: You don’t need CSS for that. As I was writing it, it morphed into more—a look at a few things I’ve learned by building atop the Global Styles Engine in WordPress.
There is so much more to explore with this system. It really unlocks untold potential. The more you and I use it, the more feedback we can leave to contributors to iterate on the features in the UI. For example, I would love to see some experimentation around the newer section and text-based block styles to have their own selectors in the UI.
My original premise for this post still holds true, sort of. I don’t need to write nearly as much CSS as I once did. That seems to be a good thing for my theme users, as they can see those design choices reflected in the UI and can customize how they like.
Resources
This tutorial was a small taste of what’s possible with theme.json
and style variations. Check out these resources to learn more:
- Global Settings and Styles (theme.json)
- Mixing and matching styles, colors, and typography in WordPress 6.6
- Styling sections, nested elements, and more with Block Style Variations in WordPress 6.6
- Adding and using custom settings in theme.json
- Customizing core block style variations via theme.json
- Per-block CSS with theme.json
Props to @marybaum and @bph for copyediting this post, making it much better than it was in the first draft.
Leave a Reply