The Astro island vs script choice is about how much state a component has. For interactions where the HTML barely changes (opening a menu, switching tabs, copying text, toggling dark mode), a <script> in an Astro component is smaller and simpler. When the UI re-renders from data (filters that update a list, multi-step forms, live search, anything with lots of state), a framework island with React, Svelte, Vue, Solid or Preact pays for its runtime.
Key Takeaways
- Script: a few hundred bytes, no runtime, works on every page that renders the component.
- Island: a framework runtime plus your code, but declarative state and re-rendering.
- If you already have the component in React, using it as an island is usually fine.
- Two or three small islands from one framework cost one runtime; a second framework doubles it.
Decision flow
- Does the interaction only toggle attributes or classes? (menu, accordion, tabs, dialog, copy) → script.
- Does it fetch data and render a list or results? → island (or a script, if the list is simple).
- Does it have several pieces of state that depend on each other? (filters, wizard, cart editor) → island.
- Does an existing, tested component already exist in a framework? → island; don’t rewrite working code.
- Will many instances appear on one page? → script, which Astro includes once regardless of instance count.
Compared
Script in .astro |
Framework island | |
|---|---|---|
| Bundle | Your code only | Framework runtime + your code |
| Server HTML | Yes, the component’s markup | Yes (except client:only) |
| State | Manual: DOM attributes, variables | Declarative |
| Re-rendering | You update the DOM | Framework does it |
| Load timing | Deferred module after parse | Controlled by client:* |
| Many instances | Script included once | Each instance hydrates |
| Testing | DOM tests | Framework testing tools |
The same accordion both ways
As a script:
---
// src/components/Accordion.astro
const { title } = Astro.props;
---
<div class="accordion">
<button class="accordion-trigger" aria-expanded="false">{title}</button>
<div class="accordion-panel" hidden><slot /></div>
</div>
<script>
document.querySelectorAll<HTMLButtonElement>('.accordion-trigger').forEach((btn) => {
btn.addEventListener('click', () => {
const open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
(btn.nextElementSibling as HTMLElement).hidden = open;
});
});
</script>
As a React island:
// src/components/Accordion.tsx
import { useState, type ReactNode } from 'react';
export default function Accordion({ title, children }: { title: string; children: ReactNode }) {
const [open, setOpen] = useState(false);
return (
<div className="accordion">
<button aria-expanded={open} onClick={() => setOpen(!open)}>{title}</button>
<div hidden={!open}>{children}</div>
</div>
);
}
<Accordion client:visible title="Shipping">...</Accordion>
Both are accessible and behave the same. The script version ships no framework; the island ships React. For an accordion, the script wins. For a product filter with ten checkboxes driving a results list, the island’s state model is worth it.
A middle path
Libraries that enhance HTML without a virtual DOM, such as Alpine.js, or web components, sit between the two. They work in Astro with a <script> import and suit teams who want declarative markup without a full framework.
Directive choice for islands is covered in which client directive to use, and the cost of each island in finding what’s bloating your Astro bundle. The Astro components guide covers both patterns, and the official client-side scripts guide explains script processing.
FAQ
Do scripts in Astro components run once per component instance?
The script is included once per page. Write it to find and set up every instance, as in the accordion example.
Can a script and an island share state?
Yes, through the DOM (custom events, data attributes) or a framework-neutral store such as Nano Stores.