Scoped Styles
In React you scope styles to a component by naming your file Button.module.css and importing it — the bundler rewrites class names to unique hashes at build time. In Astro you write plain CSS inside a <style> block in the same .astro file. The compiler adds a unique data attribute hash to every selector and every HTML element in that component, producing the same effect with zero configuration.
Side-by-side comparison
Section titled “Side-by-side comparison”// Button.tsx + Button.module.css// Button.module.css.button { background: #6366f1; color: #fff; border-radius: 6px; padding: .5rem 1rem; border: none; cursor: pointer; }.button:hover { background: #4f46e5; }
// Button.tsximport styles from './Button.module.css';export default function Button({ label }: { label: string }) { return <button className={styles.button}>{label}</button>;}---// Button.astroconst { label } = Astro.props;---<button class="button">{label}</button>
<style> .button { background: #6366f1; color: #fff; border-radius: 6px; padding: .5rem 1rem; border: none; cursor: pointer; } .button:hover { background: #4f46e5; }</style>What the Astro compiler produces under the hood:
<!-- compiled output — you never write this manually --><button class="button astro-aBcD1234">Click me</button>
<style> .button.astro-aBcD1234 { background: #6366f1; ... }</style>The hash suffix (astro-aBcD1234) is injected automatically. A .button rule in a different component gets a different hash, so the styles never collide.
What can go inside the style block
Section titled “What can go inside the style block”The <style> block accepts standard CSS — custom properties, media queries, pseudo-selectors, nesting, @keyframes, anything your target browsers support. No CSS-in-JS API to learn.
// Card.module.css.card { border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem; transition: transform .15s; }.card:hover { transform: translateY(-2px); }@media (max-width: 480px) { .card { padding: .75rem; } }
// Card.tsximport styles from './Card.module.css';export default function Card({ title }: { title: string }) { return <div className={styles.card}><h2>{title}</h2></div>;}---// Card.astroconst { title } = Astro.props;---<div class="card"><h2>{title}</h2></div>
<style> .card { border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem; transition: transform .15s; } .card:hover { transform: translateY(-2px); } @media (max-width: 480px) { .card { padding: .75rem; } }</style>Runnable example
Section titled “Runnable example”---
const items = ["Dashboard", "Projects", "Settings"];
---
<html lang="en">
<head><meta charset="utf-8" /><title>Scoped styles</title></head>
<body>
<nav>
{items.map(item => (
<a href="#" class="nav-link">{item}</a>
))}
</nav>
<main>
<div class="card">
<h1>Scoped Styles Demo</h1>
<p>The <code>.card</code> and <code>.nav-link</code> styles live in this file and are scoped here by the Astro compiler — they cannot bleed into other components.</p>
</div>
</main>
</body>
</html>
<style>
body { font-family: sans-serif; margin: 0; background: #f8fafc; }
nav { background: #1e293b; padding: .75rem 1.5rem; display: flex; gap: 1rem; }
.nav-link { color: #94a3b8; text-decoration: none; font-size: .875rem; }
.nav-link:hover { color: #fff; }
main { padding: 2rem; }
.card { background: #fff; border: 1px solid #e2e8f0; border-radius: 10px; padding: 1.5rem; max-width: 480px; }
.card h1 { font-size: 1.25rem; margin: 0 0 .75rem; color: #1e293b; }
.card p { color: #64748b; line-height: 1.6; margin: 0; }
code { background: #f1f5f9; padding: .1em .35em; border-radius: 4px; font-size: .875em; }
</style>