This article is not a definitive guide to :has(). It’s also not here to regurgitate what’s already been said. It’s just me (hi 👋) jumping on the bandwagon for a moment to share some of the ways I’m most likely to use :has() in my day-to-day work… that is, once it is officially supported by Firefox which is imminent.
When that does happen, you can bet I’ll start using :has() all over the place. Here are some real-world examples of things I’ve built recently and thought to myself, “Gee, this’ll be so much nicer once :has() is fully supported.”
Avoid having to reach outside your JavaScript component
Have you ever built an interactive component that sometimes needs to affect styles somewhere else on the page? Take the following example, where <nav> is a mega menu, and opening it changes the colors of the <header> content above it.
I feel like I need to do this kind of thing all the time.
This particular example is a React component I made for a site. I had to “reach outside” the React part of the page with document.querySelector(...) and toggle a class on the <body>, <header>, or another component. That’s not the end of the world, but it sure feels a bit yuck. Even in a fully React site (a Next.js site, say), I’d have to choose between managing a menuIsOpen state way higher up the component tree, or do the same DOM element selection — which isn’t very React-y.
With :has(), the problem goes away:
header:has(.megamenu--open) {
/* style the header differently if it contains
an element with the class ".megamenu--open"
*/
}
No more fiddling with other parts of the DOM in my JavaScript components!
Better table striping UX
Adding alternate row “stripes” to your tables can be a nice UX improvement. They help your eyes keep track of which row you’re on as you scan the table.
But in my experience, this doesn’t work great on tables with just two or three rows. If you have, for example, a table with three rows in the <tbody> and you’re “striping” every “even” row, you could end up with just one stripe. That’s not really worth a pattern and might have users wondering what’s so special about that one highlighted row.
What to get fancier? You could also decide to only do this if the table has at least a certain number of columns, too:
table:has(:is(td, th):nth-child(3)) {
/* only do stuff if there are three or more columns */
}
Remove conditional class logic from templates
I often need to change a page layout depending on what’s on the page. Take the following Grid layout, where the placement of the main content changes grid areas depending on whether there’s a sidebar present.
That’s something that might depend on whether there are sibling pages set in the CMS. I’d normally do this with template logic to conditionally add BEM modifier classes to the layout wrapper to account for both layouts. That CSS might look something like this (responsive rules and other stuff omitted for brevity):
/* m = main content */
/* s = sidebar */
.standard-page--with-sidebar {
grid-template-areas: 's s s m m m m m m m m m';
}
.standard-page--without-sidebar {
grid-template-areas: '. m m m m m m m m m . .';
}
CSS-wise, this is totally fine, of course. But it does make the template code a little messy. Depending on your templating language it can get pretty ugly to conditionally add a bunch of classes, especially if you have to do this with lots of child elements too.
Contrast that with a :has()-based approach:
/* m = main content */
/* s = sidebar */
.standard-page:has(.sidebar) {
grid-template-areas: 's s s m m m m m m m m m';
}
.standard-page:not(:has(.sidebar)) {
grid-template-areas: '. m m m m m m m m m . .';
}
Honestly, that’s not a whole lot better CSS-wise. But removing the conditional modifier classes from the HTML template is a nice win if you ask me.
It’s easy to think of micro design decisions for :has() — like a card when it has an image in it — but I think it’ll be really useful for these macro layout changes too.
Better specificity management
If you read my last article, you’ll know I’m a stickler for specificity. If, like me, you don’t want your specificity scores blowing out when adding :has() and :not() throughout your styles, be sure to use :where().
That’s because the specificity of :has() is based on the most specific element in its argument list. So, if you have something like an ID in there, your selector is going to be tough to override in the cascade.
/* specificity score: 0,1,0.
Same as a .standard-page--with-sidebar
modifier class
*/
.standard-page:where(:has(.sidebar)) {
/* etc */
}
The future’s bright
These are just a few things I can’t wait to be able to use in production. The CSS-Tricks Almanac has a bunch of examples, too. What are you looking forward to doing with :has()? What sort of some real-world examples have you run into where :has() would have been the perfect solution?
While I am not a regular Chrome extension programmer, I have certainly coded enough extensions and have a wide enough web development portfolio to know my way around the task. However, just recently, I had a client reject one of my extensions as I received feedback that my extension was “outdated”.
As I was scrambling to figure out what was wrong, I swept my embarrassment under the carpet and immediately began my deep dive back into the world of Chrome Extensions. Unfortunately, information on Manifest V3 was scarce and it was difficult for me to understand quickly what this transition was all about.
Needless to say, with a pending job, I had to painstakingly navigate my way around Google’s Chrome Developer Documentation and figure things out for myself. While I got the job done, I did not want my knowledge and research in this area to go to waste and decided to share what I wish I could have had easy access to in my learning journey.
Why the transition to Manifest 3 is important
Manifest V3 is an API that Google will use in its Chrome browser. It is the successor to the current API, Manifest V2, and governs how Chrome extensions interact with the browser. Manifest V3 introduces significant changes to the rules for extensions, some of which will be the new mainstay from V2 we were used to.
Manifest V3 will officially begin rolling out in January 2023.
By June 2023, extensions that run Manifest V2 will no longer be available on the Chrome Web Store.
Extensions that do not comply with the new rules introduced in Manifest V3 will eventually be removed from the Chrome Web Store.
One of the main goals of Manifest V3 is to make users safer and improve the overall browser experience. Previously, many browser extensions relied on code in the cloud, meaning it could be difficult to assess whether an extension was risky. Manifest V3 aims to address this by requiring extensions to contain all the code they will run, allowing Google to scan them and detect potential risks. It also forces extensions to request permission from Google for the changes they can implement on the browser.
Staying up-to-date with Google’s transition to Manifest V3 is important because it introduces new rules for extensions that aim to improve user safety and the overall browser experience, and extensions that do not comply with these rules will eventually be removed from the Chrome Web Store.
In short, all of your hard work in creating extensions that used Manifest V2 could be for naught if you do not make this transition in the coming months.
January 2023
June 2023
January 2024
Support for Manifest V2 extensions will be turned off in Chrome’s Canary, Dev, and Beta channels.
The Chrome Web Store will no longer allow Manifest V2 extensions to be published with visibility set to Public.
The Chrome Web Store will remove all remaining Manifest V2 extensions.
Manifest V3 will be required for the Featured badge in the Chrome Web Store.
Existing Manifest V2 extensions that are published and publically visible will become unlisted.
Support for Manifest 2 will end for all of Chrome’s channels, including the Stable channel, unless the Enterprise channel is extended.
The key differences between Manifest V2 and V3
There are many differences between the two, and while I highly recommend that you read up on Chrome’s “Migrating to Manifest V3” guide, here is a short and sweet summary of key points:
Network request modification is handled with the new declarativeNetRequest API in Manifest V3.
In Manifest V3, extensions can only execute JavaScript that is included within their package and cannot use remotely-hosted code.
Manifest V3 introduces promise support to many methods, though callbacks are still supported as an alternative.
Host permissions in Manifest V3 are a separate element and must be specified in the "host_permissions" field.
The content security policy in Manifest V3 is an object with members representing alternative content security policy (CSP) contexts, rather than a string as it was in Manifest V2.
In a simple Chrome Extension’s Manifest that alters a webpage’s background, that might look like this:
// Manifest V2
{
"manifest_version": 2,
"name": "Shane's Extension",
"version": "1.0",
"description": "A simple extension that changes the background of a webpage to Shane's face.",
"background": {
"scripts": ["background.js"],
"persistent": true
},
"browser_action": {
"default_popup": "popup.html"
},
"permissions": [ "activeTab", ],
"optional_permissions": ["<all_urls>"]
}
// Manifest V3
{
"manifest_version": 3,
"name": "Shane's Extension",
"version": "1.0",
"description": "A simple extension that changes the background of a webpage to Shane's face.",
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html"
},
"permissions": [ "activeTab", ],
"host_permissions": [ "<all_urls>" ]
}
If you find some of the tags above seem foreign to you, keep reading to find out exactly what you need to know.
How to smoothly transition to Manifest V3
I have summarized the transition to Manifest V3 in four key areas. Of course, while there are many bells and whistles in the new Manifest V3 that need to be implemented from the old Manifest V2, implementing changes in these four areas will get your Chrome Extension well on the right track for the eventual transition.
The four key areas are:
Updating your Manifest’s basic structure.
Modify your host permissions.
Update the content security policy.
Modify your network request handling.
With these four areas, your Manifest’s fundamentals will be ready for the transition to Manifest V3. Let’s look at each of these key aspects in detail and see how we can work towards future-proofing your Chrome Extension from this transition.
Updating your Manifest’s basic structure
Updating your manifest’s basic structure is the first step in transitioning to Manifest V3. The most important change you will need to make is changing the value of the "manifest_version" element to 3, which determines that you are using the Manifest V3 feature set.
One of the major differences between Manifest V2 and V3 is the replacement of background pages with a single extension service worker in Manifest V3. You will need to register the service worker under the "background" field, using the "service_worker" key and specify a single JavaScript file. Even though Manifest V3 does not support multiple background scripts, you can optionally declare the service worker as an ES Module by specifying "type": "module", which allows you to import further code.
In Manifest V3, the "browser_action" and "page_action" properties are unified into a single "action" property. You will need to replace these properties with "action" in your manifest. Similarly, the "chrome.browserAction" and "chrome.pageAction" APIs are unified into a single “Action” API in Manifest V3, and you will need to migrate to this API.
Overall, updating your manifest’s basic structure is a crucial step in the process of transitioning to Manifest V3, as it allows you to take advantage of the new features and changes introduced in this version of the API.
Modify your host permissions
The second step in transitioning to Manifest V3 is modifying your host permissions. In Manifest V2, you specify host permissions in the "permissions" field in the manifest file. In Manifest V3, host permissions are a separate element, and you should specify them in the "host_permissions" field in the manifest file.
Here is an example of how to modify your host permissions:
In order to update the CSP of your Manifest V2 extension to be compliant with Manifest V3, you will need to make some changes to your manifest file. In Manifest V2, the CSP was specified as a string in the "content_security_policy" field of the manifest.
In Manifest V3, the CSP is now an object with different members representing alternative CSP contexts. Instead of a single "content_security_policy" field, you will now have to specify separate fields for "content_security_policy.extension_pages" and "content_security_policy.sandbox", depending on the type of extension pages you are using.
You should also remove any references to external domains in the "script-src", "worker-src", "object-src", and "style-src" directives if they are present. It is important to make these updates to your CSP in order to ensure the security and stability of your extension in Manifest V3.
The final step in transitioning to Manifest V3 is modifying your network request handling. In Manifest V2, you would have used the chrome.webRequest API to modify network requests. However, this API is replaced in Manifest V3 by the declarativeNetRequest API.
To use this new API, you will need to specify the declarativeNetRequest permission in your manifest and update your code to use the new API. One key difference between the two APIs is that the declarativeNetRequest API requires you to specify a list of predetermined addresses to block, rather than being able to block entire categories of HTTP requests as you could with the chrome.webRequest API.
It is important to make these changes in your code to ensure that your extension continues to function properly under Manifest V3. Here is an example of how you would modify your manifest to use the declarativeNetRequest API in Manifest V3:
You will also need to update your extension code to use the declarativeNetRequest API instead of the chrome.webRequest API.
Other aspects you need to check
What I have covered is just the tip of the iceberg. Of course, if I wanted to cover everything, I could be here for days and there would be no point in having Google’s Chrome Developers guides. While what I covered will have you future-proofed enough to arm your Chrome extensions in this transition, here are some other things you might want to look at to ensure your extensions are functioning at the top of their game.
Migrating background scripts to the service worker execution context: As mentioned earlier, Manifest V3 replaces background pages with a single extension service worker, so it may be necessary to update background scripts to adapt to the service worker execution context.
Unifying the**chrome.browserAction**and**chrome.pageAction**APIs: These two equivalent APIs are unified into a single API in Manifest V3, so it may be necessary to migrate to the Action API.
Migrating functions that expect a Manifest V2 background context: The adoption of service workers in Manifest V3 is not compatible with methods like chrome.runtime.getBackgroundPage(), chrome.extension.getBackgroundPage(), chrome.extension.getExtensionTabs(), and chrome.extension.getViews(). It may be necessary to migrate to a design that passes messages between other contexts and the background service worker.
Moving CORS requests in content scripts to the background service worker: It may be necessary to move CORS requests in content scripts to the background service worker in order to comply with Manifest V3.
Migrating away from executing external code or arbitrary strings: Manifest V3 no longer allows the execution of external logic using chrome.scripting.executeScript({code: '...'}), eval(), and new Function(). It may be necessary to move all external code (JavaScript, WebAssembly, CSS) into the extension bundle, update script and style references to load resources from the extension bundle, and use chrome.runtime.getURL() to build resource URLs at runtime.
Updating certain scripting and CSS methods in the Tabs API: As mentioned earlier, several methods move from the Tabs API to the Scripting API in Manifest V3. It may be necessary to update any calls to these methods to use the correct Manifest V3 API.
And many more!
Feel free to take some time to get yourself up to date on all the changes. After all, this change is inevitable and if you do not want your Manifest V2 extensions to be lost due to avoiding this transition, then spend some time arming yourself with the necessary knowledge.
On the other hand, if you are new to programming Chrome extensions and looking to get started, a great way to go about it is to dive into the world of Chrome’s Web Developer tools. I did so through a course on Linkedin Learning, which got me up to speed pretty quickly. Once you have that base knowledge, come back to this article and translate what you know to Manifest V3!
So, how will I be using the features in the new Manifest V3 going forward?
Well, to me, the transition to Manifest V3 and the removal of the chrome.webRequest API seems to be shifting extensions away from data-centric use cases (such as ad blockers) to more functional and application-based uses. I have been staying away from application development lately as it can get quite resource-intensive at times. However, this shift might be what brings me back!
The rise of AI tools in recent times, many with available-to-use APIs, has sparked tons of new and fresh SaaS applications. Personally, I think that it’s coming at a perfect time with the shift to more application-based Chrome extensions! While many of the older extensions may be wiped out from this transition, plenty of new ones built around novel SaaS ideas will come to take their place.
Hence, this is an exciting update to hop on and revamp old extensions or build new ones! Personally, I see many possibilities in using APIs that involve AI being used in extensions to enhance a user’s browsing experience. But that’s really just the tip of the iceberg. If you’re looking to really get into things with your own professional extensions or reaching out to companies to build/update extensions for them, I would recommend upgrading your Gmail account for the benefits it gives in collaborating, developing, and publishing extensions to the Chrome Web Store.
However, remember that every developer’s requirements are different, so learn what you need to keep your current extensions afloat, or your new ones going!
If you’ve ever worked on sites with lots of long-form text — especially CMS sites where people can enter screeds of text in a WYSIWYG editor — you’ve likely had to write CSS to manage the vertical spacing between different typographic elements, like headings, paragraphs, lists and so on.
It’s surprisingly tricky to get this right. And it’s one reason why things like the Tailwind Typography plugin and Stack Overflow’s Prose exist — although these handle much more than just vertical spacing.
What makes typographic vertical spacing complicated?
Surely it should just be as simple as saying that each element — p, h2, ul, etc. — has some amount of top and/or bottom margin… right? Sadly, this isn’t the case. Consider this desired behavior:
The first and last elements in a block of long-form text shouldn’t have any extra space above or below (respectively). This is so that other, non-typographic elements are still placed predictably around the long-form content.
Sections within the long-form content should have a nice big space between them. A “section” being a heading and all the following content that belongs to that heading. In practice, this means having a nice big space before a heading… but not if that heading is immediately preceded by another heading!
We want to more space above the Heading 3 when it follows a typographic element, like a paragraph, but less space when it immediately follows another heading.
You need to look no further than right here at CSS-Tricks to see where this could come in handy. Here are a couple of screenshots of spacing I pulled from another article.
The vertical spacing between Heading 2 and Heading 3The vertical space between Heading 3 and a paragraph
The traditional solution
The typical solution I’ve seen involves putting any long-form content in a wrapping div (or a semantic tag, if appropriate). My go-to class name has been .rich-text, which I think I use as a hangover from older versions of the Wagtail CMS, which would add this class automatically when rendering WYSIWYG content. Tailwind Typography uses a .prose class (plus some modifier classes).
Then we add CSS to select all typographic elements in that wrapper and add vertical margins. Noting, of course, the special behavior mentioned above to do with stacked headings and the first/last element.
The traditional solution sounds reasonable… what’s the problem?
Rigid structure
Having to add a wrapper class like .rich-text in all the right places means baking in a specific structure to your HTML code. That’s sometimes necessary, but it feels like it shouldn’t have to be in this particular case. It can also be easy to forget to do this everywhere you need to, especially if you need to use it for a mix of CMS and hard-coded content.
The HTML structure gets even more rigid when you want to be able to trim the top and bottom margin off the first and last elements, respectively, because they need to be immediate children of the wrapper element, e.g., .rich-text > *:first-child. That > is important — after all, we don’t want to accidentally select the first list item in each ul or ol with this selector.
Mixing margin properties
In the pre-:has() world, we haven’t had a way to select an element based on what follows it. Therefore, the traditional approach to spacing typographic elements involves using a mix of both margin-top and margin-bottom:
We start by setting our default spacing to elements with margin-bottom.
Next, we space out our “sections” using margin-top — i.e. very big space above each heading
Then we override those big margin-tops when a heading is followed immediately by another heading using the adjacent sibling selector (e.g. h2 + h3).
Now, I don’t know about you, but I’ve always felt it’s better to use a single margin direction when spacing things out, generally favoring margin-bottom (that’s assuming the CSS gap property isn’t feasible, which it is not in this case). Whether this is a big deal, or even true, I’ll let you decide. But personally, I’d rather be setting margin-bottom for spacing long-form content.
Collapsing margins
Because of collapsing margins, this mix of top and bottom margins isn’t a big problem per se. Only the larger of two stacked margins will take effect, not the sum of both margins. But… well… I don’t really like collapsing margins.
Collapsing margins are yet one more thing to be aware of. It might be confusing for junior devs who aren’t up to speed with that CSS quirk. The spacing will totally change (i.e. stop collapsing) if you were to change the wrapper to a flex layout with flex-direction: column for instance, which is something that wouldn’t happen if you set your vertical margins in a single direction.
I more-or-less know how collapsing margins work, and I know that they’re there by design. I also know they’ve made my life easier on occasion. But they’ve also made it harder other times. I just think they’re kinda weird, and I’d generally rather avoid relying on them.
The :has() solution
And here is my attempt at solving these issues with :has().
My solution doesn’t include all possible typographic elements. For instance, there’s no <blockquote> in my demo. The selector list is easy enough to extend though.
My solution also doesn’t handle non-typographic elements that may be present in your particular long-form text blocks, e.g. <img>. That’s because for the sites I work on, we tend to lock down the WYSIWYG as much as possible to core text nodes, like headings, paragraphs, and lists. Anything else — e.g. quotes, images, tables, etc. — is a separate CMS component block, and those blocks themselves are spaced apart from each other when rendered on a page. But again, the selector list can be extended.
I’ve only included h1 for the sake of completeness. I usually wouldn’t allow a CMS user to add an h1 via WYSIWYG, as the page title would be baked into the page template somewhere rather than entered in the CMS page editor.
I’m not catering for a heading followed immediately by the same level heading (h2 + h2). This would mean that the first heading wouldn’t “own” any content, which seems like a misuse of headings (and, correct me if I’m wrong, but it might violate WCAG 1.3.1 Info and Relationships). I’m also not catering for skipped heading levels, which are invalid.
I am in no way knocking the existing approaches I mentioned. If and when I build another Tailwind site I’ll use the excellent Typography plugin, no question!
I’m not a designer. I came up with these spacing values by eyeballing it. You probably could (and should) use better values.
Specificity and project structure
I was going to write a whole big thing here about how the traditional method and the new :has() way of doing it might fit into the ITCSS methodology… But now that we have :where() (the zero-specificity selector) you can pretty much choose your preferred level of specificity for any selector now.
That said, the fact that we’re no longer dealing with a wrapper — .prose, .rich-text, etc. — to me makes it feel like this should live in the “elements” layer, i.e. before you start dealing with class-level specificity. I’ve used :where() in my examples to keep specificity consistent. All the selectors in both of my examples have a specificity score of 0,0,1 (except for the bare-bones reset).
Wrapping up
So there you have it, a bleeding-edge solution to a very boring problem! This newer approach is still not what I’d call “simple” CSS — as I said at the beginning, it’s a more complex topic than it might seem at first. But aside from having a few slightly complex selectors, I think the new approach makes more sense overall, and the less rigid HTML structure seems very appealing.
If you end up using this, or something like it, I’d love to know how it works out for you. And if you can think of ways to improve it, I’d love to hear those too!
Someone recently asked me how I approach debugging inline SVGs. Because it is part of the DOM, we can inspect any inline SVG in any browser DevTools. And because of that, we have the ability to scope things out and uncover any potential issues or opportunities to optimize the SVG.
But sometimes, we can’t even see our SVGs at all. In those cases, there are six specific things that I look for when I’m debugging.
1. The viewBox values
The viewBox is a common point of confusion when working with SVG. It’s technically fine to use inline SVG without it, but we would lose one of its most significant benefits: scaling with the container. At the same time, it can work against us when improperly configured, resulting in unwanted clipping.
The elements are there when they’re clipped — they’re just in a part of the coordinate system that we don’t see. If we were to open the file in some graphics editing program, it might look like this:
Screenshot of SVG opened in Illustrator.
The easiest way to fix this? Add overflow="visible" to the SVG, whether it’s in our stylesheet, inline on the style attribute or directly as an SVG presentation attribute. But if we also apply a background-color to the SVG or if we have other elements around it, things might look a little bit off. In this case, the best option will be to edit the viewBox to show that part of the coordinate system that was hidden:
Demo applying overflow="hidden" and editing the viewBox.
There are a few additional things about the viewBox that are worth covering while we’re on the topic:
How does the viewBox work?
SVG is an infinite canvas, but we can control what we see and how we see it through the viewport and the viewBox.
The viewport is a window frame on the infinite canvas. Its dimensions are defined by width and height attributes, or in CSS with the corresponding width and height properties. We can specify any length unit we want, but if we provide unitless numbers, they default to pixels.
The viewBox is defined by four values. The first two are the starting point at the upper-left corner (x and y values, negative numbers allowed). I’m editing these to reframe the image. The last two are the width and height of the coordinate system inside the viewport — this is where we can edit the scale of the grid (which we’ll get into in the section on Zooming).
Here’s simplified markup showing the SVG viewBox and the width and height attributes both set on the <svg>:
The viewport we see starts where 0 on the x-axis and 0 on the y-axis meet.
By changing this:
<svg viewBox="0 0 700 700">
…to this:
<svg viewBox="300 200 700 700">
…the width and height remain the same (700 units each), but the start of the coordinate system is now at the 300 point on the x-axis and 200 on the y-axis.
In the following video I’m adding a red <circle> to the SVG with its center at the 300 point on the x-axis and 200 on the y-axis. Notice how changing the viewBox coordinates to the same values also changes the circle’s placement to the upper-left corner of the frame while the rendered size of the SVG remains the same (700×700). All I did was “reframe” things with the viewBox.
Zooming
We can change the last two values inside the viewBox to zoom in or out of the image. The larger the values, the more SVG units are added to fit in the viewport, resulting in a smaller image. If we want to keep a 1:1 ratio, our viewBox width and height must match our viewport width and height values.
Let’s see what happens in Illustrator when we change these parameters. The artboard is the viewport which is represented by a white 700px square. Everything else outside that area is our infinite SVG canvas and gets clipped by default.
Figure 1 below shows a blue dot at 900 along the x-axis and 900 along the y-axis. If I change the last two viewBox values from 700 to 900 like this:
…then the blue dot is almost fully back in view, as seen in Figure 2 below. Our image is scaled down because we increased the viewBox values, but the SVG’s actual width and height dimensions remained the same, and the blue dot made its way back closer to the unclipped area.
Figure 1
Figure 2
There is a pink square as evidence of how the grid scales to fit the viewport: the unit gets smaller, and more grid lines fit into the same viewport area. You can play with the same values in the following Pen to see that work in action:
2. Missing width and height
Another common thing I look at when debugging inline SVG is whether the markup contains the width or height attributes. This is no big deal in many cases unless the SVG is inside a container with absolute positioning or a flexible container (as Safari computes the SVG width value with 0px instead of auto). Excluding width or height in these cases prevents us from seeing the full image, as we can see by opening this CodePen demo and comparing it in Chrome, Safari, and Firefox (tap images for larger view).
Chrome
Safari
Firefox
The solution? Add a width or height, whether as a presentation attribute, inline in the style attribute, or in CSS. Avoid using height by itself, particularly when it is set to 100% or auto. Another workaround is to set the right and left values.
You can play around with the following Pen and combine the different options.
3. Inadvertent fill and stroke colors
It may also be that we are applying color to the <svg> tag, whether it’s an inline style or coming from CSS. That’s fine, but there could be other color values throughout the markup or styles that conflict with the color set on the <svg>, causing parts to be invisible.
That’s why I tend to look for the fill and stroke attributes in the SVG’s markup and wipe them out. The following video shows an SVG I styled in CSS with a red fill. There are a couple of instances where parts of the SVG are filled in white directly in the markup that I removed to reveal the missing pieces.
4. Missing IDs
This one might seem super obvious, but you’d be surprised how often I see it come up. Let’s say we made an SVG file in Illustrator and were very diligent about naming our layers so that you get nice matching IDs in the markup when exporting the file. And let’s say we plan to style that SVG in CSS by hooking into those IDs.
That’s a nice way to do things. But there are plenty of times where I’ve seen the same SVG file exported a second time to the same location and the IDs are different, usually when copy/pasting the vectors directly. Maybe a new layer was added, or one of the existing ones was renamed or something. Whatever the case, the CSS rules no longer match the IDs in the SVG markup, causing the SVG to render differently than you’d expect.
Pasting Illustrator’s exported SVG file into SVGOMG.
In large SVG files we might find it difficult to find those IDs. This is a good time to open the DevTools, inspect that part of the graphic that’s not working, and see if those IDs are still matching.
So, I’d say it’s worth opening an exported SVG file in a code editor and comparing it to the original before swapping things out. Apps like Illustrator, Figma, and Sketch are smart, but that doesn’t mean we aren’t responsible for vetting them.
5. Checklist for clipping and masking
If an SVG is unexpectedly clipped and the viewBox checks out alright, I usually look at the CSS for clip-path or mask properties that might interfere with the image. It’s tempting to keep looking at the inline markup, but it’s good to remember that an SVG’s styling might be happening elsewhere.
CSS clipping and masking allow us to “hide” parts of an image or element. In SVG, <clipPath> is a vector operation that cuts parts of an image with no halfway results. The <mask> tag is a pixel operation that allows transparency, semi-transparency effects, and blurred edges.
This is a small checklist for debugging cases where clipping and masking are involved:
Make sure the clipping path (or mask) and the graphic overlap one another. The overlapping parts are what gets displayed.
If you have a complex path that is not intersecting your graphic, try applying transforms until they match.
You can still inspect the inner code with the DevTools even though the <clipPath> or <mask> are not rendered, so use it!
Copy the markup inside <clipPath> and <mask> and paste it before closing the </svg> tag. Then add a fill to those shapes and check the SVG’s coordinates and dimensions. If you still do not see the image, try adding overflow="hidden" to the <svg> tag.
Check that a unique ID is used for the <clipPath> or <mask>, and that the same ID is applied to the shapes or group of shapes that are clipped or masked. A mismatched ID will break the appearance.
Check for typos in the markup between the <clipPath> or <mask> tags.
fill, stroke, opacity, or some other styles applied to the elements inside <clipPath> are useless — the only useful part is the fill-region geometry of those elements. That’s why if you use a <polyline> it will behave as a <polygon> and if you use a <line> you won’t see any clipping effect.
If you don’t see your image after applying a <mask>, make sure that the fill of the masking content is not entirely black. The luminance of the masking element determines the opacity of the final graphic. You’ll be able to see through the brighter parts, and the darker parts will hide your image’s content.
You can play with masked and clipped elements in this Pen.
6. Namespaces
Did you know that SVG is an XML-based markup language? Well, it is! The namespace for SVG is set on the xmlns attribute:
<svg xmlns="http://www.w3.org/2000/svg">
<!-- etc. -->
</svg>
There’s a lot to know about namespacing in XML and MDN has a great primer on it. Suffice to say, the namespace provides context to the browser, informing it that the markup is specific to SVG. The idea is that namespaces help prevent conflicts when more than one type of XML is in the same file, like SVG and XHTML. This is a much less common issue in modern browsers but could help explain SVG rendering issues in older browsers or browsers like Gecko that are strict when defining doctypes and namespaces.
The SVG 2 specification does not require namespacing when using HTML syntax. But it’s crucial if support for legacy browsers is a priority — plus, it doesn’t hurt anything to add it. That way, when the <html> element’s xmlns attribute is defined, it will not conflict in those rare cases.
This is also true when using inline SVG in CSS, like setting it as a background image. In the following example, a checkmark icon appears on the input after successful validation. This is what the CSS looks like:
When we remove the namespace inside the SVG in the background property, the image disappears:
Another common namespace prefix is xlink:href. We use it a lot when referencing other parts of the SVG like: patterns, filters, animations or gradients. The recommendation is to start replacing it with href as the other one is being deprecated since SVG 2, but there might be compatibility issues with older browsers. In that case, we can use both. Just remember to include the namespace xmlns:xlink="http://www.w3.org/1999/xlink" if you are still using xlink:href.
Level up your SVG skills!
I hope these tips help save you a ton of time if you find yourself troubleshooting improperly rendered inline SVGs. These are just the things I look for. Maybe you have different red flags you watch for — if so, tell me in the comments!
The bottom line is that it pays to have at least a basic understanding of the various ways SVG can be used. CodePen Challenges often incorporate SVG and offer good practice. Here are a few more resources to level up:
A little thing happened on the way to publishing the CSS :has() selector to the ol’ Almanac. I had originally described :has() as a “forgiving” selector, the idea being that anything in its argument is evaluated, even if one or more of the items is invalid.
/* Example: Do not use! */
article:has(h2, ul, ::-scoobydoo) { }
See ::scoobydoo in there? That’s totally invalid. A forgiving selector list ignores that bogus selector and proceeds to evaluate the rest of the items as if it were written like this:
So, our previous example? The entire selector list is invalid because the bogus selector is invalid. But the other two forgiving selectors, :is() and :where(), are left unchanged.
There’s a bit of a workaround for this. Remember, :is() and :where()are forgiving, even if :has() is not. That means we can nest either of the those selectors in :has() to get more forgiving behavior:
article:has(:where(h2, ul, ::-scoobydoo)) { }
Which one you use might matter because the specificity of :is() is determined by the most specific item in its list. So, if you need to something less specific you’d do better reaching for :where() since it does not add to the specificity score.
We updated a few of our posts to reflect the latest info. I’m seeing plenty of others in the wild that need to be updated, so just a little PSA for anyone who needs to do the same.
The good ol’ <table> tag is the most semantic HTML for showing tabular data. But I find it very hard to control how the table is presented, particularly column widths in a dynamic environment where you might not know how much content is going into each table cell. In some cases, one column is super wide while others are scrunched up. Other times, we get equal widths, but at the expense of a column that contains more content and needs more space.
But I found a CSS tricks-y workaround that helps make things a little easier. That’s what I want to show you in this post.
The problem
First we need to understand how layout is handled by the browser. We have the table-layout property in CSS to define how a table should distribute the width for each table column. It takes one of two values:
auto (default)
fixed
Let us start with a table without defining any widths on its columns. In other words, we will let the browser decide how much width to give each column by applying table-layout: auto on it in CSS. As you will notice, the browser does its best with the algorithm it has to divide the full available width between each column.
If we swap out an auto table layout with table-layout: fixed, then the browser will merely divide the full available space by the total number of columns, then apply that value as the width for each column:
But what if we want to control the widths of our columns? We have the <colgroup> element to help! It consists of individual <col> elements we can use to specify the exact width we need for each column. Let’s see how that works in with table-layout: auto:
I have inlined the styles for the sake of illustration.
The browser is not respecting the inline widths since they exceed the amount of available table space when added up. As a result, the table steals space from the columns so that all of the columns are visible. This is perfectly fine default behavior.
How does <colgroup> work with table-layout: fixed. Let’s find out:
This doesn’t look good at all. We need the column with a bunch of content in it to flex a little while maintaining a fixed width for the rest of the columns. A fixed table-layout value respects the width — but so much so that it eats up the space of the column that needs the most space… which is a no-go for us.
This could easily be solved if only we could set a min-width on the column instead of a width. That way, the column would say, “I can give all of you some of my width until we reach this minimum value.“ Then the table would simply overflow its container and give the user a horizontal scroll to display the rest of the table. But unfortunately, min-width on table columns are not respected by the <col> element.
The solution
The solution is to fake a min-width and we need to be a bit creative to do it.
We can add an empty <col> as the second column for our <colgroup> in the HTML and apply a colspan attribute on the first column so that the first column takes up the space for both columns:
Note that I have added classes in place of the inline styles from the previous example. The same idea still applies: we’re applying widths to each column.
The trick is that relationship between the first <col> and the empty second <col>. If we apply a width to the first <col> (it’s 200px in the snippet above), then the second column will be eaten up when the fixed table layout divides up the available space to distribute to the columns. But the width of the first column (200px) is respected and remains in place.
Voilà! We have a faux min-width set on a table cell. The first cell flexes as the available space changes and the table overflows for horizontal scrolling just as we hoped it would.
Let’s not totally forget about accessibility here. I ran the table through NVDA on Windows and VoiceOver on macOS and found that all five columns are announced, even if we’re only using four of them. And when the first column is in focus, it announces, “Column one through two”. Not perfectly elegant but also not going to cause someone to get lost. I imagine we could throw an aria-hidden attribute on the unused column, but also know ARIA isn’t a substitute for poor HTML.
I’ll admit, this feels a little, um, hacky. But it does work! Let me know if you have a different approach in the comments… or know of any confusions this “hack” might bring to our users.
A little while back, Ganesh Dahal penned a post here on CSS-Tricks responding to a tweet that asked about adding CSS box shadows on WordPress blocks and elements. There’s a lot of great stuff in there that leverages new features that shipped in WordPress 6.1 that provide controls for applying shadows to things directly in the Block Editor and Site Editor UI.
Ganesh touched briefly on button elements in that post. I want to pick that up and go deeper into approaches for styling buttons in WordPress block themes. Specifically, we’re going to crack open a fresh theme.json file and break down various approaches to styling buttons in the schema.
Why buttons, you ask? That’s a good question, so let’s start with that.
The different types of buttons
When we’re talking about buttons in the context of the WordPress Block Editor, we have to distinguish between two different types:
Child blocks inside of the Buttons block
Buttons that are nested inside other block (e.g. the Post Comments Form block)
If we add both of these blocks to a template, they have the same look by default.
As we can see, the HTML tag names are different. It’s the common classes — .wp-block-button and .wp-element-button — that ensure consistent styling between the two buttons.
If we were writing CSS, we would target these two classes. But as we know, WordPress block themes have a different way of managing styles, and that’s through the theme.json file. Ganesh also covered this in great detail, and you’d do well giving his article a read.
So, how do we define button styles in theme.json without writing actual CSS? Let’s do it together.
Creating the base styles
theme.json is a structured set of schema written in property:value pairs. The top level properties are called “sections”, and we’re going to work with the styles section. This is where all the styling instructions go.
We’ll focus specifically on the elements in the styles. This selector targets HTML elements that are shared between blocks. This is the basic shell we’re working with:
That button corresponds to HTML elements that are used to mark up button elements on the front end. These buttons contain HTML tags that could be either of our two button types: a standalone component (i.e. the Button block) or a component nested within another block (e.g. the Post Comment block).
Rather than having to style each individual block, we create shared styles. Let’s go ahead and change the default background and text color for both types of buttons in our theme. There’s a color object in there that, in turn, supports background and text properties where we set the values we want:
If crack open DevTools and have a look at the CSS that WordPress generates for the buttons, we see that the .wp-element-button class adds the styles we defined in theme.json:
Those are our default colors! Next, we want to give users visual feedback when they interact with the button.
Implementing interactive button styles
Since this is a site all about CSS, I’d bet many of you are already familiar with the interactive states of links and buttons. We can :hover the mouse cursor over them, tab them into :focus, click on them to make them :active. Heck, there’s even a :visited state to give users a visual indication that they’ve clicked this before.
Notice the “structured” nature of this. We’re basically following an outline:
Elements
Element
Object
Property
Value
We now have a complete definition of our button’s default and interactive styles. But what if we want to style certain buttons that are nested in other blocks?
Styling buttons nested in individual blocks
Let’s imagine that we want all buttons to have our base styles, with one exception. We want the submit button of the Post Comment Form block to be blue. How would we achieve that?
This block is more complex than the Button block because it has more moving parts: the form, inputs, instructive text, and the button. In order to target the button in this block, we have to follow the same sort of JSON structure we did for the button element, but applied to the Post Comment Form block, which is mapped to the core/post-comments-form element:
Notice that we’re no longer working in elements anymore. Instead, we’re working inside blocks which is reserved for configuring actual blocks. Buttons, by contrast, are considered a global element since they can be nested in blocks, even though they are available as a standalone block too.
The JSON structure supports elements within elements. So, if there’s a button element in the Post Comment Form block, we can target it in the core/post-comments-form block:
This selector means that not only are we targeting a specific block — we’re targeting a specific element that is contained in that block. Now we have a default set of button styles that are applied to all buttons in the theme, and a set of styles that apply to specific buttons that are contained in the Post Comment Form block.
The CSS generated by WordPress has a more precise selector as a result:
And what if we want to define different interactive styles for the Post Comment Form button? It’s the same deal as the way we did it for the default styles, only those are defined inside the core/post-comments-form block:
WordPress automagically generates and applies the right classes to output these button styles. But what if you use a “hybrid” WordPress theme that supports blocks and full-site editing, but also contains “classic” PHP templates? Or what if you made a custom block, or even have a legacy shortcode, that contains buttons? None of these are handled by the WordPress Style Engine!
No worries. In all of those cases, you would add the .wp-element-button class in the template, block, or shortcode markup. The styles generated by WordPress will then be applied in those instances.
And there may be some situations where you have no control over the markup. For example, some block plugin might be a little too opinionated and liberally apply its own styling. That’s where you can typically go to the “Advanced” option in the block’s settings panel and apply the class there:
Wrapping up
While writing “CSS” in theme.json might feel awkward at first, I’ve found that it becomes second nature. Like CSS, there are a limited number of properties that you can apply either broadly or very narrowly using the right selectors.
And let’s not forget the three main advantages of using theme.json:
The styles are applied to buttons in both the front-end view and the block editor.
Your CSS will be compatible with future WordPress updates.
The generated styles work with block themes and classic themes alike — there’s no need to duplicate anything in a separate stylesheet.
If you have used theme.json styles in your projects, please share your experiences and thoughts. I look forward to reading any comments and feedback!