Three New theme.json Powers in WordPress 7.1: Gradients Over Images, minWidth and Text Shadow
Three additions landed in theme.json within days of each other in late July. None is large on its own. Together they remove three of the more annoying reasons a block theme still needs a custom stylesheet.
One of them also fixes a genuine design flaw in how gradients have worked since block supports began.
It is worth being clear about what “removes a reason for custom CSS” means in practice, because it is easy to read as a small convenience. A block theme that needs a stylesheet needs a build step, or at minimum a file that has to be enqueued, versioned, and kept in sync with what the editor shows. Every declaration that moves into theme.json is a declaration the Site Editor can display, a site owner can change without breaking anything, and a style variation can override cleanly. The difference between a theme with 400 lines of custom CSS and one with 80 is not aesthetic – it determines how much of the design is negotiable after handover, and how much of it silently drifts out of sync with what the editor previews.
Two of these three move real declarations. The third mostly gives you a new way to make type harder to read, and it is worth understanding which is which before you start adopting.
background.gradient: the fix, not just the feature
WordPress has had gradient support for years. It has also been quietly broken in one specific way: you could never put a gradient over a background image.
The reason is a CSS detail. The existing color.gradient support stores its value at style.color.gradient and renders through the CSS background shorthand. Shorthand properties reset every longhand they cover, so the moment a gradient is applied, background-image is wiped. Gradient or image, never both.
The new background.gradient support stores at style.background.gradient and renders through the background-image longhand instead. Both values coexist in a single comma-separated declaration:
background-image: linear-gradient( 135deg, #000 0%, #fff 100% ), url( 'https://example.com/image.jpg' );
The gradient comes first in the value order, which means it layers over the image. That is the scrim pattern – a dark gradient over a photo so white text stays legible – and it has been the single most common reason to drop into custom CSS on a hero section.
Enable it in block.json:
{
"supports": {
"background": {
"backgroundImage": true,
"gradient": true,
"__experimentalDefaultControls": {
"backgroundImage": true,
"gradient": true
}
}
}
}
And set it in theme.json, globally or per block:
{
"styles": {
"background": {
"gradient": "linear-gradient( 135deg, #000 0%, #fff 100% )"
},
"blocks": {
"core/group": {
"background": {
"gradient": "var:preset|gradient|vivid-cyan-blue"
}
}
}
}
}
Five core blocks adopted it in 7.1: core/group, core/accordion, core/pullquote, core/post-content, and core/quote.
That list tells you something about intent. core/group is the wrapper most hero sections are built from, and core/post-content covers the main content area, so the two highest-value cases are handled. The presence of core/quote and core/pullquote suggests the editorial pull-quote-over-image treatment was on someone’s mind. If a block you rely on is not in that list, it can still opt in through its own block.json – the support exists independently of which blocks have adopted it so far.
Note that color.gradient has not gone anywhere. You now have two gradient supports with different storage paths and different rendering behaviour, which is a real source of confusion:
color.gradient | background.gradient | |
|---|---|---|
| Stored at | style.color.gradient | style.background.gradient |
| Renders via | background shorthand | background-image longhand |
| Coexists with an image | No – resets it | Yes – layers over it |
| Use when | Flat gradient fill, nothing underneath | An image is involved, or might be later |
The rule to remember: if a background image is involved, or might be later, use background.gradient.
What this deletes from your stylesheet
The practical win is the custom CSS you stop writing. Almost every block theme with a hero section carries some version of this – a pseudo-element used purely to fake a layer the block supports could not produce:
.hero {
position: relative;
background-image: url( 'hero.jpg' );
background-size: cover;
}
.hero::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient( 135deg, rgba( 0, 0, 0, 0.7 ) 0%, transparent 100% );
pointer-events: none;
}
.hero > * {
position: relative;
z-index: 1;
}
Nine declarations, a pseudo-element, a stacking context, and a z-index on every child to escape it – all to put a dark wash over a photo. With background.gradient the entire block collapses into one theme.json value, set through the Background panel by whoever is building the page. The stacking context goes away, which also removes the class of bug where a dropdown or sticky header inside the hero ends up trapped behind the overlay.
minWidth: the missing half of a pair
minHeight has existed in the dimensions block support for a while. minWidth now joins it, following exactly the same pattern.
In block.json:
{
"supports": {
"dimensions": {
"minWidth": true
}
}
}
In theme.json settings:
{
"settings": {
"dimensions": {
"minWidth": true
}
}
}
And in styles, globally or per block:
{
"styles": {
"dimensions": {
"minWidth": "400px"
},
"blocks": {
"core/group": {
"dimensions": {
"minWidth": "400px"
}
}
}
}
}
It applies the CSS min-width property and supports dimension presets (dimensionSizes) where a theme provides them. Wiring it to presets rather than raw pixel values is worth the small extra effort, for the same reason spacing presets beat arbitrary padding: it keeps the values on a scale, and it means a site owner picking a minimum width chooses from options that were designed rather than typing a number that was not. Control visibility follows the standard design tool rules: hidden by default in the block inspector unless enabled through __experimentalDefaultControls, shown by default in Global Styles and the Site Editor. Block-level editor values override theme defaults through the normal cascade, and nothing about it is a breaking change.
The article announcing it does not list which core blocks have adopted it – this is framed as a support blocks can opt into. Check the specific blocks you care about against Beta rather than assuming parity with minHeight.
Where this matters in practice is any flex or grid child that collapses too far. A sidebar that becomes unreadable at 180px, a card in a wrapping row that squeezes below its content, a button group that folds. Those have all been custom CSS, and they are the kind of custom CSS that is easy to write and easy to forget you wrote.
The pairing with minHeight is worth thinking about deliberately rather than reflexively. minHeight is usually about presence – making a hero occupy enough of the viewport to feel like a hero. minWidth is almost always about legibility – stopping a column from becoming too narrow to read. Those are different problems, and the second one has an answer the first does not: a minWidth set in a wrapping flex container is also a breakpoint, because the moment the container cannot fit two children at their minimum width, they wrap. Setting a sensible minWidth on cards in a row often removes the need for a media query entirely.
One caution. A hard minWidth on a child inside a container narrower than that value produces overflow, and overflow on mobile is worse than a cramped column. If you set minWidth, check the narrowest viewport you support before you consider it done – the failure mode is horizontal scroll on a phone, which is the one layout bug users notice immediately and report never.
A worked hero, end to end
Putting background.gradient together with an image is the case worth writing out in full, because it is the one you will reach for most.
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"color": {
"gradients": [
{
"slug": "hero-scrim",
"name": "Hero scrim",
"gradient": "linear-gradient( 180deg, rgba(6,12,24.15) 0%, rgba(6,12,24.82) 100% )"
}
]
}
},
"styles": {
"blocks": {
"core/group": {
"background": {
"gradient": "var:preset|gradient|hero-scrim"
}
}
}
}
}
Registering the scrim as a preset rather than inlining the value is the part worth copying. It gives the gradient a name in the UI, so someone building a page picks “Hero scrim” from the Background panel instead of pasting a gradient string. It also means changing the scrim across every hero on the site is a one-line edit in theme.json rather than a hunt through page content.
Note the direction of the sample gradient: nearly transparent at the top, heavily opaque at the bottom. That is the shape most photo-with-text-over-it layouts want, because the text usually sits low in the block and the top of the image is the part worth showing. A symmetrical scrim dims the whole photograph for no benefit.
Ordering, and the thing that will confuse you once
Because gradient and image render as comma-separated values on a single background-image declaration, order determines stacking, and the gradient comes first – meaning on top.
That is the opposite of the mental model most people carry from writing the pseudo-element version, where the overlay is a separate element stacked above by z-index. Here there is one property and one value list, and the first entry paints over the later ones. If your scrim appears to have no effect, check whether you have accidentally set it through color.gradient, which will have wiped the image entirely rather than layering under it.
textShadow: available, and mostly a trap
Text shadow becomes definable in theme.json. The property is textShadow and it maps directly to CSS text-shadow, so any valid value works, including multiple comma-separated shadows.
It works in three places: global typography, per-block typography, and element styles including states.
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"styles": {
"typography": {
"textShadow": "1px 1px 2px red, 0 0 1em blue, 0 0 0.2em blue"
},
"blocks": {
"core/paragraph": {
"typography": {
"textShadow": "1px 1px 2px red, 0 0 1em red, 0 0 0.2em red"
}
}
},
"elements": {
"link": {
":hover": {
"typography": {
"textShadow": "none"
}
}
}
}
}
}
The scope is narrow in this release. It is theme.json styling only – presets, block inspector controls, and per-block-instance settings are planned for the next release, and no blocks have built-in support enabled yet.
Now the part worth saying plainly: of these three additions, this is the one most likely to make your typography worse.
Text shadow degrades legibility in almost every context where body copy is involved. It reduces the effective contrast between glyph edges and background, it softens the letterforms that readers rely on to distinguish similar characters, and it compounds badly with the anti-aliasing already applied to text. A shadow that looks like depth at 200% zoom on a designer’s display reads as blur on a mid-range phone in daylight.
The example above, with its red and blue shadows, is a syntax demonstration and not a design suggestion. Treat it that way.
There are legitimate uses. Text over a photograph, where a subtle shadow buys separation that a scrim alone does not. A display heading where the effect is the point and the text is short enough that reading speed does not matter. Deliberate retro or brutalist work where degraded legibility is a stated choice.
For anything a reader has to get through – body copy, navigation, form labels, captions – the answer is no. And note that if your problem is text over an image, background.gradient from earlier in this post is the better tool. A scrim fixes contrast properly; a text shadow papers over it.
The accessibility problem, stated precisely
WCAG 2.1 contrast requirements are computed from two values: the relative luminance of the text color and the relative luminance of its background. A text shadow is neither. It sits between them visually and contributes nothing to the calculation.
That has a consequence worth being blunt about. If body text over a photograph measures 3.1:1 against the dominant background – below the 4.5:1 required for normal text at AA – adding a shadow does not move it to 4.5:1. It moves it to 3.1:1 with a shadow. Any automated checker will still fail it, correctly, and any reader who struggled before will still struggle.
What the shadow does is make the failure less obvious to the person building the page. That is the actual harm: it converts a visible problem into an invisible one, and invisible problems do not get fixed.
The correct fixes, in order of preference: raise the contrast of the text color itself; put a scrim behind the text so the background luminance is controlled and measurable; or move the text off the image entirely. All three produce a number you can verify. A shadow produces a feeling.
There is one narrow case where a shadow genuinely helps rather than hides. Text over a photograph with high local variance – a busy image where some glyphs land on light pixels and some on dark – can be legible on average and illegible in patches. A tight, low-opacity shadow adds edge definition that survives that variance. Even then it is a supplement to a scrim, never a replacement for one, and the underlying contrast still has to pass on its own.
Where a shadow is actually the right call
To be fair to the feature, three cases hold up.
Display type at large sizes where the effect is the design intent and the text is four words long. Reading speed is not the metric; presence is.
Text over video, where you cannot control what luminance is behind any given frame and a scrim alone may not be enough at every moment.
Deliberate stylistic work – retro, brutalist, editorial pastiche – where degraded legibility is a stated choice made with knowledge of the tradeoff, not an accident.
Note what none of these are: paragraphs, navigation, labels, captions, or anything a reader has to move through rather than look at.
How these fit the rest of the theme.json picture
None of these three lands in isolation, and two of them interact with changes we have already covered.
background.gradient and the responsive @ prefix syntax are natural partners. A scrim that works on a wide desktop hero is often too heavy on a phone, where the image is cropped tighter and there is less of it to see through. Being able to express the gradient responsively means the mobile hero can carry a lighter wash without a media query in a stylesheet.
minWidth sits alongside container queries rather than competing with them. A container query asks how much space a component has and restyles accordingly. A minWidth declares the floor below which the component should not be asked to cope. Used together, minWidth sets the boundary and the container query handles the range above it, which is a cleaner division than trying to make one mechanism do both. Our guide to container queries in block themes covers the other half of that pairing.
And all three continue the direction the Baseline CSS features piece described: styling capability moving out of theme stylesheets and into structured, editable style data. The practical effect for anyone maintaining a commercial theme is that the surface a site owner can change without touching CSS keeps growing, which is good for them and requires more thought from you about which of those surfaces should have sensible presets rather than raw controls.
A pre-adoption checklist
Before you enable any of these on a production theme, five things are worth confirming.
Check your existing gradient usage. Search your theme.json and any block styles for color.gradient. Anywhere a gradient sits on a block that also supports background images is a candidate to migrate, and anywhere you have a pseudo-element scrim in CSS is a candidate to delete.
Decide on presets before you ship controls. A raw gradient picker in the hands of a site owner produces gradients you would not have chosen. Registering two or three named presets and relying on those is almost always the better default.
Audit for hard-coded min-width in your stylesheet. These are the declarations minWidth replaces, and leaving both in place means a control in the editor that appears to do nothing because your CSS is winning.
Test the narrowest viewport you support. Specifically for minWidth, because overflow is the failure mode and it will not show up at desktop widths.
Decide your position on text shadow and write it down. If you are shipping a theme to other people, the useful thing is not whether you personally use it. It is whether a site owner who enables it gets any guardrails. Consider whether your theme should expose it at all.
What to adopt
background.gradient is the one to reach for immediately once 7.1 ships. It fixes a real limitation, it has clear core block adoption, and it deletes custom CSS from the most common hero pattern there is.
minWidth is straightforwardly useful with no caveats, and it costs one line wherever you currently have a min-width override.
textShadow is worth knowing exists and worth using sparingly. Add it to your theme.json vocabulary; keep it out of your body copy.
All three ship with WordPress 7.1 on 19 August. If you are working through a block theme audit before then, the first two belong on the “delete custom CSS” list and the third belongs on the “review before anyone enables it” list.
That split is the useful way to hold this release generally. A lot of the 7.1 theme changes are migrations with deadlines attached, and they get the attention because something breaks if you ignore them. These three are the other category: nothing breaks, nothing is deprecated, and the only cost of ignoring them is that you keep maintaining code you no longer need to maintain.
The gradient fix in particular is worth doing deliberately rather than opportunistically. The pseudo-element scrim pattern is in almost every commercial block theme, it is copied between projects, and it carries a stacking context that causes a specific and irritating class of bug. Replacing it is a contained piece of work with a clear end state, and it is the kind of cleanup that gets indefinitely deferred unless someone schedules it. Schedule it.