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.

src/components/layout.astro
---
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>
src/components/heading.astro
---
const { title } = Astro.props;
---
<h1 class="heading">{title}</h1>
<style>
.heading {
color: red;
}
</style>
src/pages/index.astro
---
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.

an Astro site showing a red heading

The Astro site generated by the 3 files above.

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.

Chromium devtools open to highlight the generated data attribute on an Astro component's rendered HTML

Example of the generated data attribute used to scope Astro component styles

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:

src/pages/index.astro
---
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.

devtools output showing the override crossed out because the component CSS specificity is higher

The override has lower specificity, so it’s ignored.

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.

src/pages/index.astro
---
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:

src/components/heading.astro
---
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:

src/pages/index.astro
---
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:

src/components/layout.astro
---
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>

an Astro site showing a blue heading

The CSS layers override applied, resulting in a blue headline.

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.

Resources