WordPress theme.json pseudo-selectors now work per block

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 :visited and 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.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.