CSS overrides without important using layers in Astro components
We used to need `!important` to override styles, but it’s not 2021 anymore and there’s a better way: CSS layers.
Recently I was working on a shared Astro component and found an edge case that required me to override the CSS of the Astro component from within the page that imported it.
I thought the fix was neat, so I figured I’d write it up in case it helps anyone else.
Minimal reproduction of CSS override challenges is Astro
To see the problem in action, let’s imagine a simple Astro site that has a layout, a heading component, and a page that imports that heading component.
---const { title } = Astro.props;---
<html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <meta name="viewport" content="width=device-width" /> <meta name="generator" content={Astro.generator} /> <title>{title}</title> </head> <body> <slot /> </body></html>
<style> html, body { font-family: system-ui, sans-serif; margin: 20px; text-align: center; }</style>
---const { title } = Astro.props;---
<h1 class="heading">{title}</h1>
<style> .heading { color: red; }</style>
---import Layout from '../components/layout.astro';import Heading from '../components/heading.astro';
const title = 'Style overrides in Astro with CSS layers';---
<Layout title={title}> <Heading title={title} /></Layout>
These three files result in a simple Astro site.
Why standard overrides don’t work in Astro components
One of Astro’s strengths is that, by default, all CSS is scoped to the component. This solves one of the biggest sources of frustration that many devs feel with CSS: the cascade and inheritance, which can become really hard to keep track of in large code bases.
The way Astro does this is by applying a unique generated data
attribute to the built HTML, which is then appended to every CSS selector.
The problem comes in when we do want to use the cascade. For example, if an edge case arises where the headline needs to be blue, we may try something like adding a global override to the .heading
class in the page using the Heading
component:
---import Layout from '../components/layout.astro';import Heading from '../components/heading.astro';
const title = 'Style overrides in Astro with CSS layers';---
<Layout title={title}> <Heading title={title} /></Layout>
<style is:global> .heading { color: blue; }</style>
This won’t work. If we inspect the element, we can see that the scoped component style has higher specificity, so we can’t override it this way.
If we want to override the CSS in an Astro component, we’re going to need to try something else.
CSS overrides in Astro the old way: !important
Previously, I would have reluctantly slapped an !important
at the end of the override. It’s heavy-handed and feels kinda gross, but it works. Them’s the breaks.
---import Layout from '../components/layout.astro';import Heading from '../components/heading.astro';
const title = 'Style overrides in Astro with CSS layers';---
<Layout title={title}> <Heading title={title} /></Layout>
<style is:global> .heading { /* not ideal */ color: blue !important; }</style>
But it’s not 2021 anymore! We have a better option: enter CSS layers.
CSS overrides in Astro the new way: CSS layers
Since March 2022, @layer
is part of Baseline.
This means we can assign a given rule or rules to a named layer. For example, we can define a layer called component
for our heading component styles:
---const { title } = Astro.props;---
<h1 class="heading">{title}</h1>
<style> @layer component { .heading { color: red; } }</style>
Next, we can define an additional layer called overrides
in the page that imports the component:
---import Layout from '../components/layout.astro';import Heading from '../components/heading.astro';
const title = 'Style overrides in Astro with CSS layers';---
<Layout title={title}> <Heading title={title} /></Layout>
<style is:global> @layer overrides { .heading { color: blue !important; color: blue; } }</style>
Because these layers are arbitrarily named, there’s no magic involved — we can either rely on the layers to be applied in the order they were defined (which will get very confusing with components), or we can manually define the layer order.
To manually define the layer order, add a @layer
declaration to the top of the layout component that tells the browser to render the component styles first, then apply the overrides:
---const { title } = Astro.props;---
<html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <meta name="viewport" content="width=device-width" /> <meta name="generator" content={Astro.generator} /> <title>{title}</title> </head> <body> <slot /> </body></html>
<style> @layer component, overrides;
html, body { font-family: system-ui, sans-serif; margin: 20px; text-align: center; }</style>
Because of how cascade layers work, we don’t need the !important
hack to override the component styles because our layer order tells the browser that the overrides take precedence.
Modern CSS overrides, no hacks
CSS layers are great because they give all the control of things like !important
, but without the compounding frustration of ever-increasing specificity wars. As developers, we can choose what takes precedence by placing our styles into layers — and we choose the order of importance for the layers.
I was really happy to see this feature land in all modern browsers, and even happier that Astro is built in such a way that we can use the platform to accomplish edge case goals like these.