Skip to content
Astro as scripts 4 min read

Scoped Styles

Any <style> tag you write inside an Astro component is automatically scoped to that component. This means a rule like h1 { color: red } only affects the <h1> elements rendered by that component, not headings elsewhere on the page. Scoping is the default behavior, requires zero configuration, and lets you write simple, semantic selectors without worrying about naming collisions across a large codebase.

How scoping works

When Astro compiles a component, it generates a unique hash for that component and rewrites your CSS selectors to include a matching data-astro-cid-* attribute. It then stamps that same attribute onto every element the component renders. Because the selector and the markup share the hash, the styles can only ever match elements from this component.

Consider a Card.astro component:

---
const { title } = Astro.props;
---

<article class="card">
  <h2>{title}</h2>
  <slot />
</article>

<style>
  .card {
    border: 1px solid #e2e8f0;
    border-radius: 0.5rem;
    padding: 1rem;
  }
  h2 {
    margin: 0 0 0.5rem;
    font-size: 1.25rem;
  }
</style>

The browser receives HTML and CSS that look roughly like this:

<article class="card" data-astro-cid-h3x2k1>
  <h2 data-astro-cid-h3x2k1>My Title</h2>
</article>

<style>
  .card[data-astro-cid-h3x2k1] { ... }
  h2[data-astro-cid-h3x2k1] { ... }
</style>

The bare h2 selector is now h2[data-astro-cid-h3x2k1], so it can never style an <h2> rendered by a different component. The specificity bump is intentional but kept low (a single attribute selector) so your styles remain easy to override.

Scoping is class-based by attribute, not by Shadow DOM. The styles still live in the global stylesheet, so devtools, theming, and CSS custom properties all work exactly as you’d expect — there is no encapsulation boundary to fight.

What scoping does and does not reach

Scoped styles apply to the elements your component author writes directly in the template. They do not automatically reach into the rendered output of child components, because each child owns its own scope.

TargetReached by parent’s scoped styles?
Elements written in the component’s own templateYes
Elements passed into a <slot />Yes (they receive the parent scope)
The internal markup of a child componentNo (the child has its own scope)
Elements created later by client-side JavaScriptNo

This is why slotted content is styled by the component that provides it, not the one that renders the <slot />. Markup injected after hydration won’t carry the data attribute either, so dynamic DOM needs global styles or inline styles.

Reaching child components with :global()

When you genuinely need to style a descendant component’s internals, wrap the selector in :global(). This opts that selector out of scoping while leaving the rest of the <style> block scoped.

---
import Prose from "../components/Prose.astro";
---

<div class="content">
  <Prose />
</div>

<style>
  /* scoped: only this component's .content */
  .content {
    max-width: 65ch;
  }

  /* global: targets links inside the rendered Prose output */
  .content :global(a) {
    color: var(--accent);
    text-decoration: underline;
  }
</style>

Prefixing with a scoped selector (.content :global(a)) keeps the blast radius small: only anchors inside this component’s .content are affected, rather than every link on the page. Using a bare :global(a) would style the entire site.

Combining scoped styles with frameworks

Because Astro emits zero JavaScript by default, scoped styles are pure CSS shipped statically — there is no runtime cost. When you embed an interactive island, the island’s own framework styling rules apply inside it, while the surrounding .astro markup keeps Astro’s attribute scoping.

---
import Counter from "../components/Counter.jsx";
---

<section class="widget">
  <Counter client:visible />
</section>

<style>
  .widget {
    display: grid;
    gap: 1rem;
  }
</style>

Here .widget is scoped to this component. The Counter island hydrates on client:visible, but its internal DOM is owned by React and is not reached by .widget’s scoped rules — use :global() or style the island within its own framework component instead.

Best practices

  • Lean on scoping: write semantic, low-specificity selectors (h2, .title) rather than long, defensive class chains.
  • Reserve :global() for the few cases where you must cross a component boundary, and always anchor it to a scoped parent selector to limit reach.
  • Keep cross-cutting concerns (resets, typography, design tokens) in a dedicated global stylesheet rather than duplicating them in every component.
  • Remember that slotted content inherits the provider’s scope, so style slotted markup where you write it.
  • Use CSS custom properties for theming — they pass through scope boundaries cleanly and pair well with define:vars.
  • Don’t rely on scoped styles for DOM created by client-side scripts; that markup lacks the scope attribute.
Last updated June 14, 2026
Was this helpful?