
How to add global styles in Astro?
The simplest way to add global styles in Astro is to create a CSS file, for example src/styles/global.css, and import it in the frontmatter of your base layout with import "../styles/global.css";. Every page that uses that layout gets the stylesheet, and Astro bundles, minifies and caches it for you. Rules in an imported file are not scoped, so body, a or h1 selectors apply everywhere the file is loaded. For smaller cases you can also use <style is:global> inside a component, or a <link> tag pointing at a file in public/.
Astro scopes <style> tags in components by default, which is great for components but means you need a deliberate place for resets, design tokens and typography that should apply site-wide. In this article you'll learn the recommended layout-import approach, when to use is:global and :global() instead, how to load CSS from npm packages and from public/, how to style rendered Markdown, how Astro orders global and scoped CSS in the cascade, and how the CSS ends up in your built pages.
Option 1: Import a Stylesheet in Your Layout
This is the approach the Astro docs recommend and the one I'd use for almost every project.
Step 1: Create the Stylesheet
Put your global rules in a regular CSS file under src/:
/* src/styles/global.css */
:root {
--color-text: #1f2328;
--color-bg: #ffffff;
--color-accent: #0969da;
--font-body: system-ui, sans-serif;
--space: 1rem;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: var(--font-body);
color: var(--color-text);
background: var(--color-bg);
line-height: 1.6;
}
a {
color: var(--color-accent);
}
Keeping the file in src/ rather than public/ matters: files in src/ go through Vite, so they're minified, bundled with other CSS and given hashed filenames for long-term caching.
Step 2: Import It in the Base Layout
---
// src/layouts/BaseLayout.astro
import "../styles/global.css";
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>
The import path is relative to the component file, and CSS imports go at the top of the frontmatter with your other imports.
Step 3: Use the Layout on Every Page
---
// src/pages/index.astro
import BaseLayout from "../layouts/BaseLayout.astro";
---
<BaseLayout title="Home">
<h1>Home</h1>
<p>Read the <a href="/blog/post">post</a>.</p>
</BaseLayout>
Any page wrapped in BaseLayout now has the global stylesheet. If you haven't set up layouts yet, the post on reusable layouts in Astro walks through the pattern.
Why the Layout Is the Right Place
Imported CSS in Astro applies to every page where the importing component is part of the module graph. It isn't scoped to the component that imports it. Putting the import in one base layout gives you a single, obvious place where global CSS enters the site, and every page that should look the same gets the same file.
Option 2: is:global Inside a Component
You can turn off scoping for a whole <style> block with is:global:
---
// src/layouts/BaseLayout.astro
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>My site</title>
</head>
<body>
<slot />
</body>
</html>
<style is:global>
body {
margin: 0;
font-family: system-ui, sans-serif;
}
</style>
This works, and it's handy for a few rules that belong to one layout. For anything larger, a separate .css file is easier to maintain, works with editor tooling and can be shared between layouts.
Option 3: :global() for Descendants
:global() is different: it keeps part of the selector scoped and makes the rest global. It's the right tool when one component needs to style content it doesn't write itself, such as rendered Markdown passed through a slot:
---
// src/layouts/PostLayout.astro
import BaseLayout from "./BaseLayout.astro";
const { frontmatter } = Astro.props;
---
<BaseLayout title={frontmatter.title}>
<article class="prose">
<h1>{frontmatter.title}</h1>
<slot />
</article>
</BaseLayout>
<style>
.prose :global(h2) {
margin-top: 2.5rem;
}
.prose :global(code) {
font-size: 0.9em;
background: #f6f8fa;
padding: 0.1em 0.3em;
}
</style>
In the build, those rules compile to .prose[data-astro-cid-ssdmjifj] h2 and .prose[data-astro-cid-ssdmjifj] code. Only headings and code inside this layout's .prose wrapper are affected, which is much safer than a site-wide h2 rule. The mechanics are explained in detail in the post on scoped CSS in Astro.
Loading CSS from an npm Package
Resets and utility libraries are often published to npm. If the package documents an import path with a file extension, import it like a local file:
---
// src/layouts/BaseLayout.astro
import "modern-normalize/modern-normalize.css";
import "../styles/global.css";
---
I tested this with modern-normalize 3.0.1, and the reset ended up in the page's CSS ahead of the site's own rules, which is exactly the order you want.
If a package tells you to import a path without an extension, such as package-name/normalize, Astro may fail to prerender the page. The fix is to add the package to Vite's ssr.noExternal list:
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
vite: {
ssr: {
noExternal: ["package-name"],
},
},
});
Despite the name, this is a Vite bundling setting and has nothing to do with on-demand rendering.
Option 4: A link Tag to a File in public/
Files in public/ are copied to the build output untouched. You can link to them with an absolute path:
<head>
<!-- Served from public/styles/print.css -->
<link rel="stylesheet" href="/styles/print.css" media="print" />
</head>
The href must be an absolute path (starting with /) or a full URL. Relative paths aren't supported. Because these files skip Vite, they aren't minified, bundled or hashed, and you're responsible for cache headers. Use this approach for files that must keep a fixed URL, or for third-party stylesheets on a CDN. For your own site CSS, an import is almost always better.
Comparing the Options
| Approach | Processed by Vite | Scope | Best for |
|---|---|---|---|
import "./global.css" in a layout | Yes | Every page using the layout | Resets, tokens, base typography |
<style is:global> | Yes | Pages that render the component | A handful of layout-specific rules |
.wrapper :global(el) | Yes | Descendants of one wrapper | Markdown and CMS content |
<link href="/file.css"> | No | Every page with the tag | Fixed-URL or third-party CSS |
How Global and Scoped Styles Combine
When rules conflict, the browser checks specificity first and then the order rules appear. Astro emits CSS in this order, from lowest to highest precedence:
<link>tags in the head.- Imported stylesheets, in the order they're imported.
- Scoped
<style>blocks from components.
That's why the layout import pattern works so well: global rules come first and act as defaults, then each component's scoped styles override them where needed. A scoped h1 { color: red; } beats a global h1 { color: black; } because it comes later and, with the default scoping strategy, carries a little extra specificity.
Two practical consequences:
- Import global CSS first: the docs recommend importing your layout (or your global stylesheet) before other imports so it has the lowest precedence.
- Watch for leaks from imports: CSS imported by any component applies once that component is part of the page, even if it's never rendered. Keep component-specific CSS in scoped
<style>blocks rather than imported files.
Using Cascade Layers for Extra Control
If you combine a reset, a third-party library and your own rules, CSS cascade layers make the order explicit, regardless of specificity. This is plain CSS, so it works in any imported file:
/* src/styles/global.css */
@layer reset, base, utilities;
@layer base {
body {
font-family: var(--font-body);
}
}
@layer utilities {
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}
Unlayered styles, which includes Astro's scoped component styles, take precedence over layered ones of normal importance, so components still override your base layer.
Using Design Tokens Everywhere
Custom properties defined in :root in your global stylesheet are available in every scoped style block, because CSS variables inherit through the DOM, not through components:
---
// src/components/Callout.astro
---
<aside class="callout">
<slot />
</aside>
<style>
.callout {
padding: var(--space);
border-left: 4px solid var(--color-accent);
}
</style>
This is the cleanest way to keep a consistent look: tokens are global, while the rules that use them stay scoped. The same tokens become the switch for themes; the post on adding dark mode to Astro builds on exactly this setup.
How Global CSS Is Delivered in Production
Astro splits CSS per page and pulls shared CSS into separate chunks. Small chunks are inlined into a <style> tag in the head; anything above 4kB is linked as an external file. You can control this with build.inlineStylesheets:
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
build: {
inlineStylesheets: "never",
},
});
With 'never', my two-page test produced a shared BaseLayout.[hash].css file linked from both pages, plus a separate file for the Markdown layout's styles:
dist/_astro/BaseLayout.CjKc5m5v.css
dist/_astro/post.C1s-AqTf.css
The shared file is cached after the first page view, which helps on larger sites. 'always' inlines everything, and the default 'auto' picks per file based on size. If you're using a utility framework or a preprocessor, the same import approach applies; see the posts on Tailwind CSS in Astro and Sass in Astro.
FAQ: Global Styles in Astro
Create a file such as src/styles/global.css and import it in the frontmatter of your base layout. Keeping it in src/ means Vite bundles, minifies and fingerprints it for caching.
Not directly. In your own project code, stylesheets are imported from components or pages. The usual pattern is one import in a base layout that every page uses, which has the same effect as a site-wide stylesheet.
is:global makes every rule in a style block unscoped. :global() only unscopes part of a selector, such as .prose :global(h2), so the rule still depends on an element owned by the component.
Astro places scoped component styles after imported stylesheets, and the default scoping adds a little specificity. That's intentional: global CSS acts as a default and components refine it.
Only if it needs a fixed URL or comes from a third party. Files in public/ are copied as-is and skip bundling, minification and hashing, so imported CSS from src/ is better for your own styles.
Import a stylesheet in the layout that wraps your Markdown, or use a wrapper class with :global() selectors in that layout. The wrapper approach keeps the rules limited to article content.
Conclusion
Global styles in Astro come down to one habit: put site-wide CSS in a file under src/styles/ and import it at the top of your base layout. That gives you bundling, minification and caching, and it lets scoped component styles override your defaults naturally, because Astro always emits imported CSS before scoped CSS.
Reach for :global() when a component needs to style content it doesn't own, is:global for a few layout-specific rules, and <link> tags only for files that must keep a fixed URL. With design tokens defined once in :root, every component can share the same colours and spacing while keeping its own rules safely scoped.


