Using CSS Custom Properties in Astro

3 min read

Updated:

Introduction

CSS scoping is a way to restrict the rules of styles to specific parts of your HTML document, preventing global styles from affecting unintended elements. It helps you manage styles more effectively, especially in large projects or when components need isolated styles.

Astro is a meta-framework which uses this kind of scoping by default. This makes it challenging to use CSS Custom properties, but not impossible.

Example Components

Let’s say you have a parent component:

<div class="block prose mx-auto">
  <h1 transition:name={frontmatter.slug} class="post-title pb-0">
    {frontmatter.title}
  </h1>
  <div class="inline-flex items-center gap-3">
    <p class="my-0">
      <time datetime={formatToTimestamp(frontmatter.publishDate)}>{formatToDisplayTime(frontmatter.publishDate)}</time>
    </p>
    &bull;
    <ul class="inline-flex my-0 pl-0 list-none">
      {authors.map((author) => (
        <li>
          <AuthorWithPicture author={author} textStyle="inline" />
        </li>
      ))}
    </ul>
  </div>
  <article>
    <slot />
  </article>
</div>

The AuthorWithPicture component looks like this:

---
import { Image } from 'astro:assets';
import type { CollectionEntry } from 'astro:content';

export interface Props {
  author: CollectionEntry<'author'>;
  textStyle: 'inline' | 'stacked';
  class?: string;
}

const { author, textStyle = 'stacked', class: className = '', ...rest } = Astro.props;
---

<div class={`author-picture relative flex items-center gap-x-4 ${className}`} {...rest}>
  {
    author.data.avatar ? (
      <Image src={author.data.avatar} alt={author.data.avatarAlt} class="h-10 w-10 rounded-full bg-gray-50" />
    ) : null
  }
  <div class:list={['text-sm leading-6', { 'flex items-baseline gap-4': textStyle === 'inline' }]}>
    <p class="name">
      <a href={`/authors/${author.id}`}>
        <span class="absolute inset-0"></span>
        {author.data.name}
      </a>
    </p>
    <p class="title">{author.data.title}</p>
  </div>
</div>

<style>
  p {
    margin: 0;
  }
  p.title {
    color: var(--author-title-color);
  }
  p.name {
    @apply font-semibold;
    color: var(--author-name-color);
  }
</style>

You want to Style the AuthorWithPicture component. What element do you target? What class? You aren’t able to treat a Component like this:

/* INCORRECT */
AuthorWithPicture {
  --author-name-color: theme('colors.gray.900');
  --author-title-color: theme('colors.gray.600');
}

In frameworks that use scoped styles, there are different ways to approach this. So you will have to refer to your Framework of choice on the best way to do it, but at least in Astro, the recommended approach is to pass a class to the child component:

<AuthorWithPicture author={author} textStyle="inline" class="override-css-variables" />

// ...

<style>
  .override-css-variables {
    --author-title-color: green;
    --author-name-color: red;
  }
</style>

Then inside the AuthorWithPicture component you’ll destructure and rename the prop. JavaScript has a reserved keyword named class so we have to call it something else:

export interface Props {
  author: CollectionEntry<'author'>;
  textStyle: 'inline' | 'stacked';
  class?: string;
}

const { author, textStyle = 'stacked', class: className = '', ...rest } = Astro.props;
//                                       ^        ^-- renamed to `className`
//                                       |-- reserved keyword in JavaScript

Now we can use className in our mark-up, I typically put it on the root element, since CSS Custom Properties cascade, but you can put it anywhere you deem fit:

<div class={className} {...rest}>

The Astro Docs say:

Using the default scoped style strategy, you must also pass the data-astro-cid-* attribute. You can do this by passing the ...rest of the props to the component. If you have changed scopedStyleStrategy to 'class' or 'where', the ...rest prop is not necessary.

So the ...rest property may not be needed in your specific case, but I included it as a point of demonstration.

Conclusion

Utilizing this strategy unblocked me from being able to style portions of my website, so I hope it can help you in the same way.