Styles & Scripts
Scoped styles are the default
Section titled “Scoped styles are the default”A <style> block inside a .astro file is scoped to that component automatically. Astro rewrites your selectors (adding a per-component hash) so a rule for h1 here cannot bleed into another component’s h1.
<h1>Title</h1><p>Body</p><style> h1 { color: rebeccapurple; } /* only THIS component's h1 */ p { line-height: 1.6; }</style>You write plain, obvious selectors and get component-local CSS — no naming conventions, no CSS-in-JS. This is the default you will use almost everywhere.
Opting out with is:global
Section titled “Opting out with is:global”Sometimes you genuinely want a rule to apply site-wide — a CSS reset, base typography, a theme. Add is:global to opt a whole block out of scoping:
<style is:global> :root { --brand: #883AEA; } body { margin: 0; font-family: system-ui; }</style>You can also scope-escape a single selector with the :global() function inside an otherwise-scoped block:
<style> /* scoped, except this one rule reaches into slotted/child content */ :global(.markdown-body a) { color: var(--brand); }</style>Passing values into CSS with define:vars
Section titled “Passing values into CSS with define:vars”Frontmatter values are server-side; CSS is static text. To bridge them, define:vars injects JavaScript values as CSS custom properties on the style block:
---const accent = "#883AEA";const gap = "1.5rem";---<div class="card">Styled by props</div><style define:vars={{ accent, gap }}> .card { border: 2px solid var(--accent); padding: var(--gap); }</style>Astro emits an inline style carrying --accent and --gap, so a prop can drive the styling without any client JavaScript. It is the clean way to make a component’s colors or spacing configurable by props.
The <script> tag: processed and bundled by default
Section titled “The <script> tag: processed and bundled by default”A <script> in a .astro file runs in the browser — but Astro treats it as a module: it is processed, bundled, TypeScript-compiled, and its imports are resolved, just like any other JS in your build.
<button id="menu">Menu</button><script> // Bundled module script. Runs in the browser. import { toggle } from '../scripts/menu'; document.getElementById('menu')?.addEventListener('click', toggle);</script>This is real client JavaScript — the one place in this module where code reaches the browser. It is perfect for small, imperative DOM work: wiring an event listener, reading localStorage, initializing a third-party widget.
Opting out with is:inline
Section titled “Opting out with is:inline”If you need the script emitted exactly as written — not bundled or processed (for an inline analytics snippet, or a script that must run before hydration) — add is:inline:
<script is:inline> window.dataLayer = window.dataLayer || [];</script>Inline scripts skip bundling, so their import statements are not resolved and they are not deduplicated across the page. Use them only when you specifically need the raw, untouched tag.
The crucial point: a script is NOT an island
Section titled “The crucial point: a script is NOT an island”This trips up newcomers. A <script> runs in the browser, but it does not make the surrounding component an island. There is no component hydration, no framework, no re-rendering — it is just a plain script that manipulates the DOM Astro already produced.
<!-- This runs, but nothing is "hydrated". It's a script, not an island. --><div id="counter">0</div><script> let n = 0; const el = document.getElementById('counter'); document.getElementById('inc')?.addEventListener('click', () => { el.textContent = String(++n); });</script>When you want component interactivity — a React/Vue/Svelte component with state that hydrates — that is an island, created with a client directive, and it is the entire next module. Rule of thumb: reach for a <script> for small imperative DOM tasks; reach for an island when you want a stateful UI component running on the client.
flowchart TB scr["plain script tag"] --> dom["runs in browser, manipulates existing DOM"] dom --> nohy["no component hydration"] isl["island with a client directive"] --> hyd["framework component hydrates"] hyd --> state["has its own state and re-renders"]