You wrote a clean CSS rule. You checked the specificity. You even threw in !important as a last resort. And still—the front end shows a different color, a weird font size, or spacing that doesn’t match your code. If you’re building or maintaining a modern block theme, the troublemaker is usually sitting right there in plain sight: the theme.json file. Figuring out why its settings walk all over your styles—and how to steer that behavior—is a core skill for any WordPress systems engineer who works with the Site Editor.

The Hidden Style Engine Inside theme.json
When the Site Editor and block themes landed in WordPress, they brought a centralized configuration file along for the ride: theme.json. This file does a lot more than list color palettes and font sizes. It’s a style generator. The settings you put inside theme.json get parsed by WordPress and turned into real CSS custom properties and class-based rules. Those rules get injected straight into the page—often with a specificity that catches developers off guard.
The generated styles end up inside inline <style> blocks with IDs like global-styles-inline-css. Because they’re printed late in the <head> and sometimes carry surprisingly high specificity, they can override stylesheets you’ve enqueued properly. This isn’t a bug. It’s a deliberate design choice that gives theme authors a structured way to enforce design tokens. But when you don’t know the mechanism, it just feels like your CSS is being ignored for no good reason.
How theme.json Generates CSS
WordPress reads theme.json and maps its presets to CSS custom properties on the :root selector. A color palette defined under settings.color.palette becomes a set of --wp--preset--color--* variables. Typography settings become --wp--preset--font-size--*, and so on. Those variables then get referenced by utility classes that both the editor and the front end use.
But the real override muscle comes from the styles section. When you declare something like this:
{
"styles": {
"color": {
"text": "#111111"
},
"typography": {
"fontSize": "18px"
}
}
}
WordPress doesn’t just set a variable. It outputs a CSS rule that targets the body element directly. That rule gets printed inside an inline style block that loads after your theme’s stylesheet. The cascade order and the element-level selector often give it more weight than a class-based rule in your stylesheet. The result? Your carefully crafted .entry-content p rule loses to the raw body declaration generated from theme.json.
Why Your Stylesheet Loses the Specificity Battle
Plenty of developers assume that a stylesheet enqueued with a high priority will always win. But the cascade isn’t just about load order. It’s about selector strength, source order, and—in the block theme world—where the rule originates. The theme.json styles section produces rules that are often element-level selectors with the same or higher specificity than your class-based rules. When both rules have equal specificity, the one that comes last in the document wins. Since the theme.json inline styles are printed after your enqueued stylesheets, they take precedence.
Here’s a common scenario: you set a base font size in your stylesheet on the body element. Then you define a different base font size inside theme.json under styles.typography.fontSize. The generated inline CSS also targets body, and it appears later in the HTML. Your stylesheet rule gets overridden. The same thing happens with link colors, heading sizes, and spacing presets. The cascade isn’t broken—it’s just working against your expectations.

Specificity Traps in Global Styles
The styles section inside theme.json applies rules at the document level. But the specificity trap gets deeper when you use the elements sub-section. Styling all links via styles.elements.link.color.text generates a rule that targets a directly. If your theme stylesheet uses .entry-content a, the theme.json rule may still win because it’s applied later and often carries the same or higher specificity due to how WordPress outputs the CSS.
Block-level styles defined in theme.json under styles.blocks are even more aggressive. They generate compound selectors that are hard to beat without cranking up specificity in your own stylesheet. A button block style might output as .wp-block-button__link combined with additional classes, making it a pain to override with a simple custom class.
Controlling the Cascade Without Fighting It
The goal isn’t to ditch theme.json or to litter your stylesheet with !important flags. The goal is to understand the cascade order and use the right tool for each layer of the design system. WordPress gives you several clean ways to manage this.
1. Use theme.json for Design Tokens, Not All Styles
Treat theme.json as the single source of truth for design tokens: colors, font sizes, spacing units, and base element styles that should be consistent everywhere. Avoid duplicating these in your stylesheet. If you want a different color for a specific context, use a CSS custom property that references the preset, not a hardcoded hex value. This keeps the cascade predictable and makes global style changes trivial.
For example, instead of overriding the link color in your stylesheet, define the link color inside theme.json under styles.elements.link.color.text. Then, for exceptions, use a more specific selector in your stylesheet that references the preset variable. That way, the base rule comes from theme.json, and your overrides are intentional and scoped.
2. Use the CSS Custom Properties theme.json Creates
Every preset you define in theme.json becomes a CSS custom property. You can use these variables in your own stylesheets to maintain consistency while controlling the cascade. If you set a primary color in theme.json, it becomes var(--wp--preset--color--primary). You can then use that variable in your stylesheet rules. This approach keeps your styles in sync with the design tokens and avoids specificity conflicts because you’re not re-declaring the same property on the same element.
This technique is especially handy for component-level styling. Instead of fighting the global link color, you can write a rule like .special-link { color: var(--wp--preset--color--primary); }. The variable resolves to the value from theme.json, but your selector controls where it applies. No cascade battle, just clean composition.
3. Disable Specific theme.json Outputs When Needed
Sometimes you need to prevent theme.json from generating certain CSS rules entirely. WordPress provides filters for this. The wp_theme_json_data_theme filter lets you modify the theme.json data before it’s processed. You can unset specific style keys to stop the corresponding CSS from being output.
For example, to remove the default link color and let your stylesheet handle it completely, add this to your theme’s functions.php:
add_filter( 'wp_theme_json_data_theme', function( $theme_json ) {
$data = $theme_json->get_data();
unset( $data['styles']['elements']['link'] );
$theme_json->update_with( $data );
return $theme_json;
});
This approach gives you surgical control. You can remove specific presets, entire style blocks, or even disable the default duotone filters that WordPress injects. The filter runs before the CSS is generated, so the unwanted rules never reach the page.
4. Use the Editor’s Global Styles Panel Intentionally
When you or a site owner adjusts global styles through the Site Editor interface, those changes get stored in the database as user customizations. They layer on top of the theme.json settings and are printed with even higher specificity. This is by design: user preferences should override theme defaults. But if you’re building a theme for clients, you need to decide which controls to expose and which to lock down.
You can limit the Global Styles interface by defining only the presets and settings you want users to modify. If you don’t want users changing the base font size, don’t include it in settings.typography.fontSizes. If you want to allow color customization but lock down spacing, adjust the settings object accordingly. The more you define in settings, the more controls appear in the editor. The more you define in styles, the more CSS is generated. Understanding this distinction is key to controlling the cascade.

When theme.json Overrides Are a Feature, Not a Bug
There are cases where the cascade behavior of theme.json actually solves problems. Say you want to enforce brand consistency across blocks from different plugins. The global styles system makes sure third-party blocks inherit your color and typography settings. Without it, each plugin block might ship its own opinionated CSS, and you’d spend hours writing reset rules.
Similarly, when building child themes, theme.json provides a structured way to override parent theme styles without touching CSS files. A child theme’s theme.json merges with the parent’s, and the child’s values take precedence. This is cleaner than the old method of dequeuing parent stylesheets and re-enqueuing your own. The cascade is managed through a predictable JSON merge rather than stylesheet wars.
Understanding the Merge Logic
WordPress merges theme.json from three sources: core, parent theme, and child theme. Core provides the base presets and default block styles. The parent theme adds or overrides these. The child theme then overrides both. This merge happens recursively, with child values replacing parent values at the same path. The resulting merged JSON is what generates the CSS. Knowing this, you can strategically place overrides at the right level—core defaults you can’t change, but parent and child theme files are yours to control.
If you’re maintaining a site where the theme receives updates, avoid editing the parent theme.json directly. Instead, use a child theme’s theme.json to override specific styles. This keeps your customizations safe during theme updates and gives you a clear record of what you’ve changed.
Debugging When theme.json Steals Your Styles
When a style isn’t applying, the browser’s developer tools are your first stop. Inspect the element and look at the computed styles. The winning rule will be listed, along with its source. If the source is an inline <style> block with an ID like global-styles-inline-css, you know theme.json is responsible. From there, you can trace the exact property and decide how to handle it.
Sometimes the override is coming from user customizations saved in the database. These appear as a separate inline style block with a class like .wp-duotone-* or custom properties on :root. If you see values that don’t match your theme.json, check the Global Styles revisions in the database or reset them via the Site Editor.
Another debugging tactic: temporarily remove the styles section from your theme.json and see if the problem disappears. If it does, you’ve confirmed the source. Then you can selectively restore parts of the styles section until you isolate the offending declaration.
Using theme.json to Reduce Stylesheet Bloat
Once you understand how theme.json generates CSS, you can use it to replace large portions of your stylesheet. Instead of writing reset rules for every block, define base styles in theme.json under styles.blocks. This centralizes your design system and reduces the amount of custom CSS you need to maintain. The generated styles are scoped to the blocks and load only when the blocks are present, which is a performance win.
For example, rather than writing a stylesheet rule for every heading level, define them in theme.json under styles.elements. The output is clean, specific, and automatically respects the editor UI. Your stylesheet can then focus on layout, component variations, and interactions—things theme.json doesn’t handle.
FAQ
Why does my custom CSS for buttons get ignored even with high specificity?
Button blocks receive styles from multiple sources in the Site Editor era. The theme.json file generates rules that target .wp-block-button__link with pseudo-classes like :hover and :focus. These generated rules often have specificity that matches or exceeds your custom rules, and they load after your stylesheet. To regain control, either override the button styles within theme.json under styles.blocks.core/button, or use the wp_theme_json_data_theme filter to remove the button styles entirely and handle them in your stylesheet with carefully scoped selectors.
Can I completely disable the CSS output from theme.json?
Yes, but it’s rarely the best move. You can remove the default global styles by filtering wp_theme_json_data_theme and unsetting the entire styles key. However, this also removes the CSS custom properties for your presets, which means you lose the design token system. A more balanced approach is to disable only the specific style outputs that conflict with your stylesheet, keeping the preset variables intact for use in your own CSS.
How do I stop users from overriding my theme styles through the Site Editor?
Control what appears in the Global Styles panel by limiting the settings object in theme.json. If you don’t define a setting for typography, the typography panel won’t appear. You can also use the block_editor_settings_all filter to disable specific features. For a more locked-down approach, combine a minimal theme.json with custom CSS that doesn’t rely on the global styles system for critical design elements.
If you’re troubleshooting broader site visibility issues while working on theme styles, the article What to Fix First When a New WordPress Site Says Nothing Found covers common configuration gaps that can affect how your theme renders.
Building a Predictable Styling Pipeline
The tension between theme.json and traditional CSS isn’t a flaw. It’s a shift in how WordPress organizes its design layer. By treating theme.json as the design token authority and your stylesheet as the component and layout layer, you create a predictable pipeline. The cascade becomes a tool rather than an obstacle.
Start by auditing your current theme.json for style declarations that duplicate your stylesheet. Move those into one place or the other based on whether they are global design tokens or context-specific rules. Use the filter API to remove generated CSS that you want to handle manually. And always check the browser’s computed styles panel when something doesn’t look right—the source of the winning rule will tell you exactly where to make the fix.
When the cascade is under your control, you spend less time debugging and more time building. That’s the whole point of systems engineering in WordPress.