Skip to content

Server-Only & Scripts

This is the concept that surprises React developers most. In React, every component you write ships JavaScript to the browser — even a component that just renders a heading. Astro inverts this default: a plain .astro component produces zero JavaScript.

React
// Heading.jsx — React
// This component ships JS to the browser.
// React needs a runtime to mount it, diff it,
// and re-render it if props change.
export default function Heading({ title }) {
return <h1>{title}</h1>;
}
// React also ships event-wiring, reconciler, etc.
// even when none of that is needed here.
Astro
---
// Heading.astro — Astro
// This compiles to:
// <h1>My Title</h1>
// No JavaScript. No runtime. No hydration.
const { title } = Astro.props;
---
<h1>{title}</h1>

The Astro compiler turns Heading.astro into a function that returns an HTML string. The browser receives that string. Done. No script tags, no React runtime, nothing.

What you cannot do in a plain .astro component

Section titled “What you cannot do in a plain .astro component”

Because the frontmatter runs at build/server time and the template produces static HTML, you cannot:

  • Call useState, useReducer, or any React hook.
  • Attach event listeners in the frontmatter (no onClick that runs in the browser).
  • Read window, document, or localStorage in the frontmatter (they do not exist at build time).
  • Make the UI react to user input — that requires an island (covered in the next module).

Adding vanilla browser JS with a script tag

Section titled “Adding vanilla browser JS with a script tag”

When you need a small amount of browser JavaScript — a toggle, a counter, an analytics event — Astro lets you include a <script> tag directly in the template. Astro bundles and deduplicates these scripts automatically.

React
// Counter.jsx — React
// Ships React + hydration + component JS
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Astro
---
// Counter.astro — vanilla JS via <script>
// Ships only the tiny script below — no framework runtime
---
<div>
<p id="count">Count: 0</p>
<button id="btn">Increment</button>
</div>
<script>
let count = 0;
const display = document.getElementById('count');
const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
count++;
display.textContent = 'Count: ' + count;
});
</script>

The <script> tag in an Astro component is processed by Vite: it is bundled, tree-shaken, and injected once per page even if the component is used multiple times. For complex interactivity, use an island (a React, Preact, Svelte, or other framework component with a client:* directive).

Astro
---
const items = ["Astro", "React", "TypeScript", "Vite"];
---
<html lang="en">
  <head><meta charset="utf-8" /><title>Script demo</title></head>
  <body style="font-family:sans-serif;padding:2rem">
    <h1>Tech Stack</h1>
    <ul id="list" style="padding-left:1.5rem">
      {items.map((item) => <li>{item}</li>)}
    </ul>
    <button id="toggle" style="margin-top:1rem;padding:.5rem 1rem;cursor:pointer">
      Toggle list
    </button>
  </body>
</html>

<script>
  const list = document.getElementById('list');
  const btn = document.getElementById('toggle');
  btn.addEventListener('click', () => {
    list.style.display = list.style.display === 'none' ? '' : 'none';
  });
</script>
How much JavaScript does a plain .astro component send to the browser?
You need a click handler that toggles a menu in an .astro component. What is the lightest approach that stays within .astro (no island)?
What happens to a `<script>` tag inside an .astro component when the component is used multiple times on the same page?
Which of these CAN you do inside an Astro component's frontmatter?