CSS has been around for a while, but modern features are being added faster than ever. In this post, I’ll show you 4 essential CSS capabilities: color manipulation, smart selectors, container queries, and aspect-ratio that will save you time and effort on your WordPress projects, with no JavaScript or complex workarounds required.
These days we have capabilities in CSS that would’ve been considered superpowers a mere 10 years ago. To put it in perspective: it wasn’t until around 2010 that you could even use border-radius without a vendor prefix1.
You have a ton of power and flexibility with modern CSS these days and the below features will help supercharge your workflow and let you do more things than ever before with no additional JavaScript or weird hacks required.
Let’s dive in!
1. Color manipulation
CSS now has a number of color functions and new syntax options that you can use to dynamically manipulate color values in ways that were previously impossible without JavaScript or SCSS.
These allow you to do things like mix two colors together, create new colors based on an existing color, and more.
Relative Colors
Relative color syntax allows you to take an existing color and make a new color relative to that one. This is especially powerful if you combine it with a CSS variable.
One common example is with color palettes. Your theme.json file may define a primary or brand color but now you want to also include a “shade” value that is that same color but at 20% opacity.
In the past you’d have to manually determine the final color for that semi-transparent version and then include that color itself. This isn’t the end of the world, but if that primary color changes, you have to remember to update the semi-transparent version too.
{
"color": {
"palette": [
{
"slug": "primary",
"name": "Primary",
"color": "#ff58ce"
},
{
"slug": "primary-semi-trans",
"name": "Primary Semi-Transparent",
"color": "rgb(255 88 206 / 20%)"
}
]
}
}JSONUsing the relative color syntax and the very same rgb function in CSS, I can do the exact same thing with way less work:
{
"color": {
"palette": [
{
"slug": "primary",
"name": "Primary",
"color": "#ff58ce"
},
{
"slug": "primary-semi-trans",
"name": "Primary Semi-Transparent",
"color": "rgb(from var(--wp--preset--color--primary) r g b / 20%)"
}
]
}
}JSONThe best part of the second snippet is that you don’t have to worry about the value of the starting color (--wp--preset--color--primary) here because it’s a relative color. You’re basically saying: take this color here and give me a version of it with 20% opacity.
You could also just pop a hex value in there instead of a CSS variable and it would work just the same. Even if you change the format of the color in that variable, it just works. This is really nice because you don’t always know what format a color may be in, especially if users or administrators of the site are able to change those colors.
You may notice that I’m just using the regular rgb() color function here. This would also be doable using any of the others such as: hsl, oklhc, and so on.
color-mix
This function is super handy and can be used for both relative values (similar to the above) but also as a means of mixing two colors together to make a new color. For instance, if you wanted to darken a color by 20%, you could use color mix to do that:
{
"color": {
"palette": [
{
"slug": "primary",
"name": "Primary",
"color": "#ff58ce"
},
{
"slug": "primary-dark",
"name": "Primary Dark",
"color": "color-mix(in srgb, var(--wp--preset--color--primary), black 20%)"
}
]
}
}JSONSyntax wise, what this is saying is: express the color of that CSS variable in the srgb color space and combine it with black with a 20% mix. You can change “black” to “white” and that would lighten it by 20%.
This is just one simple example of the types of things you can do with it and some of the syntax can be pretty involved, depending on how nerdy you want to get with colors. But you can use it to achieve some pretty useful results that would normally require a JavaScript library to convert colors for you.
Old Habits
- Manually mix colors by hand or using an online tool
- Relying on JavaScript to change colors dynamically
New Habits
- Use relative color syntax to alter colors in browser
- Leverage CSS variables and relative color syntax to minimize repeating code and color values
2. Smart element selectors
I almost wrote this section about just one selector but really, there’s a few super awesome ones that you may not know about and some new syntax options for older ones too. So I had to talk about a few different selectors!
:is and :where
These selectors allow you to specify a comma separated list of elements as the entire selector or as part of one. This allows you to write selectors that are much more concise and legible. See below for an example of with and without using :where.
/* Old way */
h1,
h2,
h3 {
margin-block-end: max(1rem, 1em);
margin-block-start: 0;
}
h1 a,
h2 a,
h3 a {
color: inherit;
}
/* New way */
:where(h1, h2, h3) {
margin-block-end: max(1rem, 1em);
margin-block-start: 0;
}
:where(h1, h2, h3) a {
color: inherit;
}CSSThe :is and :where selector are very similar but have one main difference: their level of specificity.
CSS Specificity is a concept that determines how specific a given selector is. This is important since a webpage may have any number of styles on it that could come from a number of places such as a stylesheet, a style tag, or an inline style on the element itself.
When the browser is displaying styles for an element it will display the most specific style if there are more than one setting the same property. The specificity of the selector is determined by the various parts of the selector using a weighting system. In most modern browsers, you can actually see the weight of a given selector by hovering over it in the dev tools.

If you’re writing styles that are meant to apply globally to all elements on a page, you typically don’t want to make those too specific. So if you’re using a selector that makes the styles specific in a way you didn’t expect, then you may find that the styles you apply from a class or on the element don’t work how you expect.
As a general rule, you should aim to make a selector only as specific as it needs to be. This helps you prevent unforseen side-effects when writing styles for dynamic pages that may contain any number of elements in various patterns or configurations.
You can read more about CSS specificity on MDN.
The :where selector does not increase the selector’s specificity. Normally if you’re using a comma separated selector or something like :is or :not the selector will become more specific. So if you didn’t want to make the style too specific but still wanted to use a more advanced selector to target your elements, the :where selector can help.
Here’s a quick example:
| Selector | Specificity (ID, class/attribute, element) |
| body p a, body h1 a | 0, 0, 3 |
| body :is(p, h1) a | 0, 0, 3 |
| body :where(p, h1) a | 0, 0, 2 |
So in the above table, the where selector has the lowest overall specificity. It has the same equivalent weight as body a, which is also 0, 0, 2.
:has
The :has selector is also called the “parent” selector because it allows you to select a parent element based on children that match the selector you pass in. There are a ton of great use cases for this that may eventually become a whole blog post unto themselves!
In WordPress terms, this selector is great for selecting a block when it contains other blocks. For instance, if I wanted to find any group block that had an h1 tag as the first block inside of it, I could do so like this:
.wp-block-group:has(h1:first-child) {
/* ... */
}CSSThe styles apply to .wp-block-group if there’s an element inside of it that matches the selector h1:first-child. This is super cool and can be a huge asset when you don’t always know the content of every single page. If you have a WordPress page that could have any number of blocks on it and in various configurations, this can be helpful to quickly and easily iron out edge cases in styling.
:nth-child superpowers
The :nth-child selector itself has been around for quite a while now, but as of 2023 or so it gained a new superpower: the selector list syntax.
This allows you to do nth-child selections on a specific selector inside of the thing you’re targeting! Let’s set up an example of what I’m talking about here to help clarify the usefulness of this.
Imagine for a second that you have a group block that contains any number of elements (headings, paragraphs, etc). Some of these elements may have a special class on them (bazinga) and you want to target the first three occurrences of that class. Let’s say your HTML looks something like this:
<div class="wp-block-group">
<p class="bazinga">....</p>
<h1>....</h1>
<p>....</p>
<p>....</p>
<h2 class="bazinga">....</h2>
<p>....</p>
<p>....</p>
<blockquote>...</blockquote>
<h3>....</h3>
<ul class="bazinga">...</ul>
<p class="bazinga">....</p>
</div>HTMLIf you were using :nth-child like before, you wouldn’t really have a clean way to do this since :nth-child targets elements based on their order in the DOM. For instance, the selector .bazinga:nth-child(-n + 3) would just target an element with the class “bazinga” if it was one of the first three children in an element.
On the otherhand, the selector :nth-child(-n + 3 of .bazinga) targets the first three elements that have the “bazinga” class. In the above example markup, the first selector example would only target the first paragraph because every other element that has the “bazinga” class is beyond the third child position.
Native CSS nesting
If you’re used to using SCSS, PostCSS, or LESS, you may be familiar with using the ampersand & as a selector and nesting styles inside each other. Historically this has not be possible to do inside a stylesheet without transpiling it first (i.e. running a build process to change the file into something the browser understands).
These days you can use the nesting and the & selector right in your CSS if you want. This can allow you to write more concise styles and is especially useful if you have to target pseudo elements. Consider the original example from the :where selector above, except now we can use the ampersand to simplify it a bit further:
:where(h1, h2, h3) {
margin-block-end: max(1rem, 1em);
margin-block-start: 0;
a {
color: inherit;
}
}
/* Pseudo example */
.bg-element {
background-image: url(...);
background-repeat: no-repeat;
position: relative;
/** Set the pseudo element as a shade over the background image */
&::before {
display: block;
content: '';
inset: 0;
position: absolute;
}
}CSSThe ampersand is not required to do nesting in modern browsers, however it can be helpful to add for clarity sake. You can also use the ampersand to target the parent element, like in the pseudo element example above.
Additionally, you can use the ampersand to reverse the selection order. For instance, if I wanted to target the class bazinga when the dark-mode class is added to the body tag, I could do so like this:
.bazinga {
body.dark-mode & {
/** Styles */
}
}CSSThe main difference between CSS nesting and nesting from SCSS, Less, et al is that you can’t use it to concatenate a selector (ex: &__element) which is a common pattern when using BEM syntax. This is a feature that, while helpful, can often be abused when used too heavily at scale. So overall I wouldn’t call it a huge disappointment that you can’t do this without using SCSS or similar tools.
Old Habits
- Use comma separated selectors like
p a, h1 a, h2 a - Rely on JavaScript to target an element based on its children or if it matches a selector regardless of source order
- Rely on SCSS or similar processing tools to use nesting syntax
New Habits
- Use
:whereand:isto cleanly target a list of selectors - Use
:whereto reduce the specificity of styles where appropriate - Use
:hasto easily target a parent element based on its children - Use the “selector list” or “of selector” syntax for nth-child to target matching elements regardless of their source order.
- Use the
&selector to handle nesting styles without using SCSS.
3. Container Queries
In the era of responsive design, media queries (@media) have long reigned supreme when styling for displays of various sizes. Media queries are always useful and some even allow you to do things beyond just checking width and height now too.
However, if you’re building a WordPress site or any other site that has dynamic content, you don’t always know where a given block or element will be placed. You might have a block that is full width, you might have one with a maximum width of 1110px, and so on. So relying a media query to do responsive styling is a bit tough because the query is based on the viewport size (i.e. the size inside the browser area and not including the scrollbar).
Now there’s a new at-rule in town that allows you to ask an element about its size to determine if you should apply styles: @container.
There is a lot I could tell you about container queries but as with some other items, that may belong in its own post entirely. But I want to show you a practical way you might use these right now.
There are a few key things to understand to start using container queries today:
- In order to use a container query, you have to start by telling the browser what element(s) are a container. You do this using the
containerproperty or thecontainer-nameandcontainer-typeproperties. - You cannot target the container itself inside a container query. You can only target the things inside the container.
- 99% of the time, the
container-typeyou want isinline-size. This allows you to query the width of the block or element in question. This ensures the best performance and doesn’t cause weird behavior for the height of the element2.
So to choose your container element correctly, think about the nearest parent element to the thing you want to target. For instance, if you’re looking to target a block and write styles based on its width, the best container is going to be the element that contains all blocks for that page.
<div class="entry-content"> <!-- Container -->
<div class="wp-block-group">...</div>
<div class="wp-block-columns">...</div>
<p>...</p>
<h2 class="wp-block-heading">...</h2>
<div class="wp-block-group">...</div>
<p>...</p>
</div>HTML.entry-content {
container: entryContent / inline-size;
}CSSIf you’re using a modern browser, your dev tools will likely show a little “pill” in the inspector that reads container after you specify an element as a container, like so:

With that out of the way, you can now write your container query:
@container (width <= 500px) {
.wp-block-group {
/** Your styles here */
}
}CSSThis CSS will target the nearest container to the matching element. So in our case we only have the one container (entryContent) so we don’t actually need to specify its name here. The browser will see the container that is a parent of the element our selector matches (.wp-block-group) and just know what to do with that.
In general, I typically recommend using the container name if there is one just so you’re 100% clear what container is being referenced. This is even more important if you’re using multiple containers on the same page. You can easily add the container name like so:
@container entryContent (width <= 500px) {
.wp-block-group {
/** Your styles here */
}
}CSSContainer queries are extremely useful if you’re needing to style the inner blocks of any block (but especially custom ones). Using these instead of a traditional media query can allow your styles to be much more reliable and overall less prone to side effects. I like these a lot because I think it reads more naturally and makes very clear sense.
Container queries make the most sense when you’re targetting things that don’t span the entire page. For instance, if you have a full-width header container (as most sites do) there isn’t much practical benefit to using a container query since the element is the full width of the viewport and always will be.
As with most things: use the right tool for the job. That said, if you’re trying to style an element in the sidebar of a blog post, using a media query would likely not be the right tool because the sidebar area is so vastly different in size to the entire viewport and eventually might stack and become wider than it displays on larger screens.
Old Habits
- Use media queries (
@media) for all responsive styles - Use JavaScript to check the size of an element before changing classes or styles.
New Habits
- Use container queries to target elements on the page based on where they are right now, instead of relative to the entire viewport.
- By default, use the
inline-sizecontainer type for best performance and minimal side effects.
4. Aspect Ratio
Let’s say you have an embed or image that you want to display in in a landscape HD format (16 / 9). This is a common thing needed to style YouTube embeds or images. In the past, it was necessary to use the padding property to ensure the height of the element remained proportional to its width3. This worked, but it was always a hack and it involved a bunch of different style properties to achieve. The newer aspect-ratio property allows you to style elements by proportion instead of by an explicit value.
You can combine aspect-ratio with the object-fit property to help constrain embeds and images quickly and easily. WordPress actually uses both of these in the image block:


Here I have the image set to an aspect-ratio of wide (16/9) and object-fit to contain, which ensures the image remains fully visible while being constrained to the appropriate proportions.
Now if I set the Scale to “Cover” that will set object-fit to cover and ensure the image is only visible inside the proportions I’ve defined:

You can use this feature in your own CSS or supported blocks to create art-directed layouts or otherwise ensure visual consistency amongst images and embeds. WordPress allows you to set various aspect-ratio presets as well in theme.json which makes this super simple to standardize even if you’re not writing much custom CSS yourself.
Old Habits
- Use padding hacks to enforce aspect ratios on elements
- Use JavaScript libraries to scale elements to a given aspect ratio
New Habits
- Use the
aspect-ratioproperty to set proportional dimensions. - Use
object-fitto determine how the image or embed should display if it doesn’t exactly match the aspect-ratio you’ve set.
Further Reading
- Using Relative Colors
- color-mix
- :is selector
- :where selector
- CSS Specificity
- :has selector
- :nth-child selector
- Container Queries (@container)
- aspect-ratio
- object-fit
- It wasn’t until around 2010 that you could use border-radius without a vendor prefix. ↩︎
- If you use
container-type: sizethis will cause the container’s height to not grow along with its content, in addition to being slower for the browser to render and parse. ↩︎ - This worked because relative padding values are relative to the width of the element. Using the 16 / 9 ratio as an example, you can figure out the padding needed by dividing 9 / 16 (.5625). So you’d set the top padding to 56.25% and then use absolute positioning on the contents so the height only comes from the padding. ↩︎