WordPress 7.0 adds native WordPress theme.json pseudo-selectors for individual blocks, which is something theme developers have been hacking around for years. I thought I had seen every workaround for styling hover states in a block theme: a dedicated CSS file loaded for one button variation, add_inline_style calls that left the source looking like a crime scene. We have tried all of it.
Pseudo-class support in theme.json used to be fenced off. You could use :hover or :focus on global HTML elements such as buttons and links, under the styles.elements key, and nowhere else. Wanting one block variation, say an “Outline” button, to change its text color on hover put you straight back into a hand-written CSS file. This update allows those selectors directly on blocks and on their variations.
The old way: a CSS workaround
Until now, giving a specific button variation a hover state usually meant something like this in the theme’s CSS file:
/* This was the only way to target block variations reliably */
.wp-block-button.is-style-outline .wp-block-button__link:hover {
background-color: var(--wp--preset--color--primary);
color: #ffffff;
}
It worked, but it broke the declarative model of block themes, and maintenance got slower because the styles for one button lived in two files at once.
Using WordPress theme.json pseudo-selectors in 7.0
WordPress 7.0 supports :hover, :focus, :focus-visible and :active. You define them inside the block’s style configuration, which leaves theme.json as the single source of truth.
An outline button variation with a native hover state looks like this:
{
"styles": {
"blocks": {
"core/button": {
"variations": {
"outline": {
"color": {
"background": "transparent",
"text": "currentColor"
},
":hover": {
"color": {
"background": "currentColor",
"text": "white"
}
}
}
}
}
}
}
}
What to watch out for
A few limits are worth knowing before you start moving states over:
- There is no UI for these states in Global Styles yet, so you hand-write them in
theme.json. - Only those four selectors work for
core/button. Sneak in:visitedand WordPress ignores it. - Styles set at block level and at variation level stay independent, so they do not conflict and you can go as granular as you need.
I wrote earlier about styling forms with theme.json, and pseudo-selector support is the next step in the same direction. If you are still fighting with modern CSS features, this takes a chunk of that work off your hands.
If theme.json pseudo-selectors are eating your dev hours, hand the job to me. I have been wrestling with WordPress since the 4.x days.
Refactoring for stability
Moving interactive states into theme.json cuts the CSS payload and keeps the theme in step with the Site Editor as it changes. You also spend less time chasing specificity problems that came from an external stylesheet.