Type something to search...
How to use Sass in an Astro project?

How to use Sass in an Astro project?

To use Sass in Astro, install the compiler with npm install -D sass (or sass-embedded), then add lang="scss" to any <style> tag in an .astro component. That's the whole setup: there's no integration to add and nothing to change in astro.config.mjs, because Astro's build tool, Vite, compiles Sass automatically as soon as the package is installed. You can also import .scss files from frontmatter, for example import "../styles/global.scss"; in a layout, and they're compiled, bundled and minified like plain CSS.

Sass styles in Astro components are still scoped, so you get nesting, variables, mixins and functions without giving up Astro's component isolation. In this article you'll learn how to install Sass, write SCSS in components, organise variables and mixins in partials with @use, add a global SCSS stylesheet, inject shared variables into every file, use the indented .sass syntax, and combine Sass with CSS custom properties and framework components. The examples were built with Astro 7.4.2 and Dart Sass 1.106.1.

Install a Sass Compiler

Pick one of the two official Dart Sass packages:

npm install -D sass
npm install -D sass-embedded

Both compile the same language. Vite uses sass-embedded if it's installed and falls back to sass otherwise, and the Vite docs recommend sass-embedded for faster builds. Avoid the long-deprecated node-sass package; it isn't supported.

Write SCSS in an Astro Component

Add lang="scss" to the style tag and write SCSS as usual:

---
// src/components/Card.astro
const { title } = Astro.props;
---
<article class="card">
  <h2 class="card__title">{title}</h2>
  <div class="card__body"><slot /></div>
</article>

<style lang="scss">
  $brand: #4f46e5;

  .card {
    padding: 1rem;
    border: 1px solid #ddd;
    border-radius: 0.5rem;

    &__title {
      margin: 0 0 0.5rem;
      color: $brand;
    }
  }
</style>

Sass runs first, then Astro scopes the result. The build output shows both steps:

.card[data-astro-cid-yk4hkwyg]{border:1px solid #ddd;border-radius:.5rem;padding:1rem}
.card__title[data-astro-cid-yk4hkwyg]{color:#4f46e5;margin:0 0 .5rem}

The &__title nesting became a flat .card__title selector, the variable was replaced with its value, and both selectors got the component's scope attribute. Everything you know about scoped CSS in Astro, including :global(), works the same inside SCSS.

Share Variables and Mixins with Partials

Variables defined inside one component aren't visible in another. To share them, put them in partials (files starting with an underscore) and load them with @use.

Create the Partials

// src/styles/_tokens.scss
$brand: #4f46e5;
$radius: 0.5rem;
$breakpoints: (
  "sm": 640px,
  "md": 768px,
  "lg": 1024px,
);
// src/styles/_mixins.scss
@use "sass:map";
@use "tokens";

@mixin up($size) {
  @media (min-width: map.get(tokens.$breakpoints, $size)) {
    @content;
  }
}

Use Them in a Component

---
// src/components/Card.astro
const { title } = Astro.props;
---
<article class="card">
  <h2 class="card__title">{title}</h2>
  <div class="card__body"><slot /></div>
</article>

<style lang="scss">
  @use "sass:color";
  @use "../styles/tokens" as *;
  @use "../styles/mixins" as mq;

  .card {
    padding: 1rem;
    border: 1px solid color.adjust($brand, $lightness: 30%);
    border-radius: $radius;

    @include mq.up("md") {
      padding: 2rem;
    }

    &__title {
      margin: 0 0 0.5rem;
      color: $brand;
    }
  }
</style>

The compiled CSS:

.card[data-astro-cid-yk4hkwyg]{border:1px solid #cfccf8;border-radius:.5rem;padding:1rem}
@media (width>=768px){.card[data-astro-cid-yk4hkwyg]{padding:2rem}}
.card__title[data-astro-cid-yk4hkwyg]{color:#4f46e5;margin:0 0 .5rem}

A few notes on the module system:

  • Paths are relative to the component: ../styles/tokens resolves to src/styles/_tokens.scss. You leave out the underscore and the extension.
  • Namespaces: by default members are accessed through the file name, such as tokens.$brand. as * drops the namespace; as mq renames it.
  • Built-in modules: functions like color.adjust() and map.get() come from sass:color and sass:map. The old global functions such as lighten() are deprecated.
  • Partials produce no CSS on their own: a file with only variables and mixins can be @used from many components without duplicating any output.

Avoid @import

Older Sass tutorials use @import "tokens";. Dart Sass now warns about it during the Astro build:

DEPRECATION WARNING [import]: Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0.

Switch to @use and @forward. The Sass team provides an automated migrator, linked from that warning, which converts most files for you.

Add a Global SCSS Stylesheet

For site-wide rules, create a normal .scss file and import it in your base layout, exactly as you would a .css file:

// src/styles/global.scss
@use "tokens";

:root {
  --brand: #{tokens.$brand};
}

body {
  margin: 0;
  font-family: system-ui, sans-serif;

  a {
    color: tokens.$brand;

    &:hover {
      text-decoration: none;
    }
  }
}
---
// src/layouts/BaseLayout.astro
import "../styles/global.scss";

const { title } = Astro.props;
---
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{title}</title>
  </head>
  <body>
    <slot />
  </body>
</html>

Imported SCSS isn't scoped, and it's placed before component styles in the cascade, so it works as a set of defaults. The general rules for this pattern are covered in the post on global styles in Astro.

The --brand: #{tokens.$brand}; line is worth highlighting. Interpolation turns a Sass variable into a CSS custom property, which makes the value available at runtime to every component, to plain CSS and to JavaScript.

Inject Shared Variables into Every File

Writing @use "../styles/tokens" as *; at the top of every component gets repetitive. Vite's css.preprocessorOptions can prepend it for you:

// astro.config.mjs
import { defineConfig } from "astro/config";

export default defineConfig({
  vite: {
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: `@use "/src/styles/tokens" as *;\n`,
        },
      },
    },
  },
});

Now any lang="scss" block can use the variables directly:

---
// src/components/Tag.astro
---
<span class="tag"><slot /></span>

<style lang="scss">
  .tag {
    background: $brand;
    border-radius: $radius;
    color: white;
  }
</style>
.tag[data-astro-cid-7t72llso]{color:#fff;background:#4f46e5;border-radius:.5rem}

Two rules for additionalData:

  • Use a root-relative path: the same string is prepended to files in different folders, so a relative path like ../styles/tokens would break. A path starting with /src/ resolves from the project root, as shown above.
  • Inject only variables, mixins and functions: anything that outputs CSS is duplicated into every stylesheet that receives it.

The scss key only applies to SCSS. If you also use the indented syntax, add a separate sass key.

Use the Indented Sass Syntax

If you prefer indentation over braces, use lang="sass":

---
// src/components/Note.astro
---
<p class="note"><slot /></p>

<style lang="sass">
@use "../styles/tokens" as *

.note
  padding: 0.75rem
  border-left: 4px solid $brand

  &:empty
    display: none
</style>

There's one trap here. The indented syntax is whitespace-sensitive, and the first line can't be indented. When I indented the block the way you normally would in an Astro file, the build failed:

[sass] Indenting at the beginning of the document is illegal.

Start the content of a lang="sass" block at column zero, as above. SCSS doesn't have this restriction.

Combine Sass Variables with CSS Custom Properties

Sass variables exist only at build time. Once compiled, $brand is just #4f46e5, and nothing can change it in the browser. CSS custom properties exist at runtime, can be overridden per element, and can be switched by a media query or a class.

FeatureSass variableCSS custom property
EvaluatedAt build timeIn the browser
Can change after loadNoYes
Works in media query conditionsYesNo
Usable from JavaScriptNoYes
Can be set from frontmatterNoYes, with define:vars

A good pattern is to keep raw values in Sass, expose the ones that need to change as custom properties, and read those in components. For values that come from props or frontmatter, see the post on define:vars in Astro.

Sass in Framework Components

Sass works in framework components too, following each framework's conventions:

  • React and Preact: import a .scss file for global styles, or use CSS Modules by naming the file .module.scss, for example import styles from "./Button.module.scss";.
  • Vue: <style lang="scss">, with or without scoped.
  • Svelte: <style lang="scss">.

The same installed sass package and the same preprocessorOptions apply. Adding a framework is covered in the post on React components in Astro.

Troubleshooting

When Sass doesn't behave as expected, these are the usual causes:

  1. Preprocessor dependency "sass-embedded" not found: neither Sass package is installed in the project. Install sass or sass-embedded as a dev dependency.
  2. Undefined variable: the file that uses the variable hasn't loaded the partial with @use, or additionalData isn't set for that syntax (scss vs sass).
  3. Can't find stylesheet to import: check the path relative to the current file, and remember that injected code needs a root-relative path.
  4. Styles not applying to child components: Sass doesn't change Astro's scoping rules. Use :global() or a wrapper element.
  5. Deprecation warnings: replace @import with @use, and global colour and map functions with their sass: module equivalents.

FAQ: Sass in Astro

No. Vite, which Astro uses for builds, compiles Sass as soon as the sass or sass-embedded package is installed. You only add lang="scss" to style tags or import .scss files.

Either works. Vite uses sass-embedded when it's installed and falls back to sass, and the Vite docs recommend sass-embedded for better performance on larger projects.

Yes. Sass compiles the block first, then Astro adds the component's scope attribute to every selector. Nesting, mixins and :global() all behave as they do in plain CSS blocks.

Set vite.css.preprocessorOptions.scss.additionalData in astro.config.mjs to a @use statement with a root-relative path, such as @use "/src/styles/tokens" as *. Only inject code that produces no CSS output.

The indented syntax doesn't allow the first line to be indented. Start the contents of the style block at column zero, or switch to lang="scss", which is not whitespace-sensitive.

You can have both in a project, but Tailwind 4 is designed to replace a preprocessor and its docs advise against combining them in the same file. Keep Tailwind in plain CSS and use Sass only in separate files if you need it.


Conclusion

Using Sass in Astro takes one install and one attribute: add sass or sass-embedded to your dev dependencies and write <style lang="scss">. Astro compiles the SCSS, scopes it to the component and bundles it like any other CSS, and .scss files imported in a layout become your global stylesheet.

For a maintainable setup, keep tokens and mixins in partials loaded with @use, inject them with additionalData if you want to skip the boilerplate, and expose values that need to change at runtime as CSS custom properties. Avoid @import, start indented .sass blocks at column zero, and Sass will fit into an Astro project without any friction.

Tags :
Share :

Related Posts

What are Astro Actions and how do they work?

What are Astro Actions and how do they work?

Astro Actions are type-safe backend functions that you define once on the server and then call from the browser as if they were ordinary async fu

Dive Deeper
What are Astro adapters and which one should you choose?

What are Astro adapters and which one should you choose?

An Astro adapter is an integration that builds the server part of your project for a specific runtime, such as Node.js, Vercel Functions, Netlify

Dive Deeper
How to build API endpoints in Astro?

How to build API endpoints in Astro?

To build an API endpoint in Astro, create a .js or .ts file in src/pages/ and export a function named after an HTTP method, such as GET or

Dive Deeper