komal.nakrani
Browse Themes
ThemesCategoriesDocsBlogTips & Tricks
Styling••By Komal Nakrani

Styling Astro Sites: Scoped CSS, Tailwind and Tokens

Astro styling options explained: scoped styles, global CSS, Tailwind CSS, design tokens, define:vars, the Fonts API, Sass and flash-free dark mode in Astro 7.

#Astro#CSS#Tailwind CSS#Design Tokens#Fonts

Astro styling starts with scoped CSS: a <style> block inside an .astro component only applies to that component. On top of that you can import global stylesheets, use Tailwind CSS, pass values into CSS with define:vars, load fonts through the built-in Fonts API, and use Sass or other preprocessors with no extra config. Astro bundles and minifies all of it, and inlines small stylesheets automatically.

This guide covers each option in Astro 7 and how to combine them into a system that makes a site easy to rebrand.

Key Takeaways

  • <style> in a component is scoped by default. Use :global() or <style is:global> for the exceptions.
  • Put site-wide resets and tokens in one global stylesheet imported by your base layout.
  • npx astro add tailwind sets up Tailwind CSS through its Vite plugin and creates a global CSS file with @import "tailwindcss".
  • CSS custom properties are the simplest design-token system and work with every option here.
  • The Fonts API (fonts in astro.config.mjs plus the <Font /> component) downloads, self-hosts and preloads web fonts for you.

Which approach to choose

Approach Best for Ships JS?
Scoped <style> Component-specific layout and visuals No
Global stylesheet Resets, typography, tokens No
Tailwind CSS Teams that prefer utility classes No
define:vars Values from props or frontmatter No
CSS Modules / Sass Existing codebases, nested syntax No
CSS-in-JS in islands Only inside framework components Sometimes

Most production Astro sites combine the first two or three. Komal themes, for example, use Tailwind CSS for layout and utilities with a small global stylesheet for tokens and base typography.

Scoped styles

Write CSS inside a component and Astro scopes it:

---
// src/components/Card.astro
---
<article class="card">
  <h3><slot name="title" /></h3>
  <slot />
</article>

<style>
  .card {
    padding: 1.5rem;
    border: 1px solid var(--color-border);
    border-radius: 0.75rem;
  }
  h3 { margin-block: 0 0.5rem; }
</style>

At build time Astro adds a data-astro-cid-* attribute (or class, depending on scopedStyleStrategy) to the elements this component renders and rewrites selectors to include it. The h3 rule above can’t leak into any other component.

With the default scopedStyleStrategy: 'attribute', scoping adds one attribute selector, so a scoped rule beats an equally specific global rule. If you want global utilities to win instead, set scopedStyleStrategy: 'where', which scopes with :where() and adds no specificity.

Styling slotted content and children

Scoping only matches elements written in the component’s own template. Content passed in through <slot />, or HTML from set:html or Markdown, is not matched. Use :global() around the part of the selector that targets it:

<div class="prose"><slot /></div>

<style>
  .prose :global(h2) { margin-top: 2.5rem; }
  .prose :global(a) { color: var(--color-accent); }
</style>

Keeping a scoped parent (.prose) in front of :global() stops the rule leaking to the rest of the page.

Passing a class into a child component

A parent’s scoped class won’t style a child component’s root unless the child accepts it. Destructure class and apply it:

---
// src/components/Button.astro
const { class: className, ...rest } = Astro.props;
---
<button class:list={['btn', className]} {...rest}><slot /></button>

Global styles

For resets, base typography and tokens, create a stylesheet and import it once in your base layout:

---
// src/layouts/BaseLayout.astro
import '../styles/global.css';
---

CSS imported in frontmatter is bundled and added to every page that uses the layout. You can also write <style is:global> inside a component, which is convenient for a few rules but harder to find later. The scoped vs global section of the Astro docs covers ordering when both apply.

Files in public/ are served untouched and are not processed. Put CSS there only if you want it linked manually and never bundled.

Tailwind CSS in Astro

Add Tailwind with one command:

npx astro add tailwind

This installs tailwindcss and @tailwindcss/vite, registers the Vite plugin in astro.config.mjs, and creates src/styles/global.css:

/* src/styles/global.css */
@import "tailwindcss";

Import that file in your layout, and utility classes work in every .astro, .mdx and framework component:

<a href="/themes/" class="inline-flex items-center rounded-lg bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">
  Browse themes
</a>

Tailwind’s current version is configured in CSS with @theme, so your tokens and Tailwind’s scale live in the same file:

@import "tailwindcss";

@theme {
  --color-brand: #2563eb;
  --font-display: "Inter", system-ui, sans-serif;
}

That makes bg-brand and font-display available as utilities. Projects started on Tailwind CSS 3 use a tailwind.config file instead. They keep working, and Tailwind publishes an upgrade tool when you decide to move. The Tailwind Astro guide has the current setup.

Use class:list to combine conditional utilities without string concatenation:

<li class:list={['px-3 py-2', { 'font-bold text-brand': isActive }]}>...</li>

Design tokens with CSS custom properties

Whatever you choose above, keep design decisions in one place. CSS custom properties work everywhere: scoped styles, global CSS, Tailwind and framework components.

/* src/styles/tokens.css */
:root {
  --color-bg: #ffffff;
  --color-text: #14140f;
  --color-muted: #5b5b55;
  --color-accent: #2563eb;
  --color-border: #e5e4df;

  --font-body: system-ui, sans-serif;
  --space-section: clamp(4rem, 8vw, 8rem);
  --radius: 0.75rem;
}

Components reference tokens, never raw values:

.card { background: var(--color-bg); border-radius: var(--radius); }

A rebrand then means editing one file, which is how a theme can be restyled in an afternoon. The styling docs for Komal themes show where each theme keeps its tokens.

define:vars for runtime values

When a value comes from props or frontmatter, define:vars turns it into scoped CSS variables:

---
// src/components/Badge.astro
const { color = '#2563eb' } = Astro.props;
---
<span class="badge"><slot /></span>

<style define:vars={{ color }}>
  .badge { background: var(--color); color: white; }
</style>

Each instance can have a different colour without inline style attributes everywhere. The define:vars tip covers the gotchas, including how it affects script bundling.

Fonts with the Fonts API

Since Astro 6, fonts have a built-in API. Declare families in config:

// astro.config.mjs
import { defineConfig, fontProviders } from 'astro/config';

export default defineConfig({
  fonts: [
    {
      provider: fontProviders.google(),
      name: 'Inter',
      cssVariable: '--font-inter',
      weights: [400, 600],
      styles: ['normal'],
    },
  ],
});

Then add the <Font /> component to your layout’s <head>:

---
import { Font } from 'astro:assets';
---
<head>
  <Font cssVariable="--font-inter" preload />
</head>

<style is:global>
  body { font-family: var(--font-inter); }
</style>

Astro downloads the files at build time, serves them from your own domain, generates @font-face rules and optimised fallbacks, and adds preload links. Visitors make no request to a third-party font CDN. Local font files are supported with the local provider. The Astro fonts guide lists every provider.

Sass, Less and Stylus

Install the preprocessor and use it with lang:

npm install -D sass
<style lang="scss">
  $gap: 1rem;
  .grid { display: grid; gap: $gap; }
</style>

No config is required; Vite handles it. Native CSS now has nesting and custom properties, so many projects don’t need Sass at all. If you already have a Sass codebase, it carries over as is. See the Sass setup tip for partials and global variables.

Dark mode without a flash

Two parts: tokens that change under a data-theme attribute, and an inline script that sets the attribute before the page paints.

/* src/styles/tokens.css */
:root { --color-bg: #ffffff; --color-text: #14140f; }
:root[data-theme='dark'] { --color-bg: #0f0f0d; --color-text: #f3f1ea; }
---
// in BaseLayout.astro <head>
---
<script is:inline>
  const stored = localStorage.getItem('theme');
  const prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
  document.documentElement.dataset.theme = stored ?? (prefersDark ? 'dark' : 'light');
</script>

is:inline matters here. A processed script is a deferred module and runs after first paint, which causes the flash. An inline script in <head> runs before the body renders.

A toggle button then flips dataset.theme and writes localStorage. If you use <ClientRouter /> for view transitions, rerun the logic on the astro:after-swap event so the attribute survives navigation.

Skip the setup

Every Komal theme ships with its tokens, fonts and Tailwind configuration done. Browse Astro themes and change the colours in one file instead of building the system yourself.

Typography for Markdown content

Markdown renders plain elements (h2, p, ul, blockquote, pre) with no classes, so long-form pages need a typography layer. You can use Tailwind’s typography plugin and wrap content in prose, or write a small stylesheet yourself:

---
// src/layouts/BlogPost.astro
---
<article class="prose">
  <slot />
</article>

<style>
  .prose { max-width: 68ch; font-size: 1.125rem; line-height: 1.7; }
  .prose :global(h2) { margin: 2.5em 0 0.75em; font-size: 1.6rem; line-height: 1.25; }
  .prose :global(h3) { margin: 2em 0 0.5em; font-size: 1.25rem; }
  .prose :global(p), .prose :global(ul), .prose :global(ol) { margin: 0 0 1.25em; }
  .prose :global(a) { color: var(--color-accent); text-underline-offset: 0.2em; }
  .prose :global(blockquote) { border-left: 3px solid var(--color-border); padding-left: 1rem; color: var(--color-muted); }
  .prose :global(pre) { padding: 1rem 1.25rem; border-radius: var(--radius); overflow-x: auto; font-size: 0.9rem; }
  .prose :global(img) { border-radius: var(--radius); }
</style>

A line length around 60 to 75 characters (68ch here) is easier to read than full-width text. Code blocks are highlighted by Shiki at build time, with no client JavaScript; choose a theme with markdown.shikiConfig.theme in astro.config.mjs, or pass themes: { light, dark } to switch with your dark mode.

Worked example: rebranding a site in one file

Here’s how the pieces fit when a client asks for new colours and fonts. Assume a Tailwind project with tokens in src/styles/global.css:

/* src/styles/global.css */
@import "tailwindcss";

@theme {
  --color-brand: #2563eb;
  --color-brand-ink: #ffffff;
  --color-surface: #ffffff;
  --color-ink: #14140f;
  --color-muted: #5b5b55;
  --radius-card: 0.75rem;
  --font-display: var(--font-inter);
  --font-body: var(--font-inter);
}

:root[data-theme='dark'] {
  --color-surface: #0f0f0d;
  --color-ink: #f3f1ea;
  --color-muted: #a3a29b;
}

Components only ever use those names:

<a href="/contact/" class="rounded-card bg-brand px-5 py-3 font-display text-brand-ink">
  Start a project
</a>

The rebrand is then three edits:

  1. Colours. Change --color-brand and --color-brand-ink in @theme. Every button, link and accent follows.
  2. Fonts. Swap the family in the fonts config (for example name: 'Fraunces' with cssVariable: '--font-fraunces') and point --font-display at it.
  3. Shape. Change --radius-card from 0.75rem to 0 for a sharper look.

No component files change. If you find yourself editing twenty components to change a colour, the design values have leaked out of the token layer. Fix that first; every future change gets cheaper.

Container queries for reusable components

Media queries respond to the viewport. A card component placed in a sidebar and in a full-width grid needs to respond to its container instead. Native CSS container queries work in all current browsers and fit Astro’s scoped styles well:

<div class="card-wrap"><article class="card"><slot /></article></div>

<style>
  .card-wrap { container-type: inline-size; }
  .card { display: grid; gap: 1rem; }

  @container (min-width: 32rem) {
    .card { grid-template-columns: 12rem 1fr; }
  }
</style>

The card switches to a side-by-side layout whenever it has room, wherever it’s placed. Tailwind supports the same idea with @container and @md: style variants.

Common styling mistakes

Fighting scoped styles with !important. If a scoped rule won’t apply to slotted or set:html content, the fix is :global() around the child selector, not !important.

Styling Markdown output with scoped selectors. <Content /> renders HTML that isn’t part of your component’s template. Wrap it in a .prose element and target .prose :global(h2).

Putting CSS in public/ expecting it to be processed. Files in public/ are served as-is: no bundling, no minification, no Tailwind. Keep stylesheets in src/.

Importing a stylesheet from many components. Each import is fine on its own, but repeated global imports make cascade order hard to predict. Import global CSS once, in the base layout.

Hard-coded colours in components. color: #2563eb in twelve files makes a rebrand a search-and-replace job. Use tokens.

Performance notes

  • Astro inlines stylesheets smaller than 4 KB by default (build.inlineStylesheets: 'auto') and links larger ones. Set it to 'always' for single-page sites or 'never' for heavy caching.
  • Scoped styles only load on pages that render the component, so unused component CSS never ships.
  • Avoid importing a large CSS framework globally when you use a fraction of it.

The Astro performance guide covers CSS delivery alongside images and scripts.

FAQ

Are Astro styles scoped by default?

Yes. A <style> block in an .astro component applies only to elements in that component’s template. Use is:global or :global() to opt out.

How do I add global CSS in Astro?

Create a CSS file, for example src/styles/global.css, and import it in the frontmatter of your base layout. Every page using that layout gets the styles.

Does Tailwind CSS work with Astro 7?

Yes. Run npx astro add tailwind to install Tailwind with its Vite plugin and create a global stylesheet that imports it.

Can I use CSS Modules in Astro?

Yes. Name the file *.module.css, import it as an object in frontmatter, and use styles.className in the template. Scoped <style> blocks usually make modules unnecessary in .astro files.

How do I style Markdown content in Astro?

Wrap the rendered <Content /> in an element with a class and target its children with :global(), for example .prose :global(h2). Tailwind users can use the typography plugin’s prose classes.