Components and layouts

Add a browser script

Distinguish a script tag that Astro bundles for the browser from frontmatter that runs only while rendering.

Frontmatter runs while Astro renders. It never reaches the browser. When the finished page needs behavior, you need a <script> tag in the template.

Here is a menu button that toggles a nav:

<button data-menu-button aria-expanded="false">Menu</button>
<nav data-menu hidden>...</nav>

<script>
  const button = document.querySelector('[data-menu-button]')
  const menu = document.querySelector('[data-menu]')

  button?.addEventListener('click', () => {
    const open = button.getAttribute('aria-expanded') === 'true'
    button.setAttribute('aria-expanded', String(!open))
    if (menu instanceof HTMLElement) menu.hidden = open
  })
</script>

Click the button and the hidden attribute flips. That’s browser code running in the browser. The frontmatter had nothing to do with it.

What Astro does with a script

Astro treats a plain <script> as a module. It bundles it, so you can import npm packages or your own files. TypeScript works inside it. And if the component appears ten times on a page, the script is included once, not ten times.

You can see this in the page source. The inline code is gone, replaced by a <script type="module" src="/_astro/…js"> tag pointing at the bundled file.

If you need the script left exactly as written, add is:inline:

<script is:inline>
  document.documentElement.dataset.theme = localStorage.theme ?? 'light'
</script>

An inline script skips bundling and deduplication. Use it for tiny things that must run before anything else, like reading a theme preference to avoid a flash of the wrong colors. Everything else, keep as a normal script.

This code is public

Anyone can read a browser script. Never put an API key, a private token, or an authorization check in it. Astro only exposes environment variables prefixed with PUBLIC_ to browser code, and that prefix is a reminder: whatever is in there, every visitor can see. If the work needs a secret, do it in frontmatter or in an endpoint.

HTML first

Before writing a script, ask if HTML already does the job. Links navigate. Forms submit. <details> and <summary> open and close without a single line of JavaScript.

The menu above is a fair case for a script, because the disclosure pattern needs aria-expanded to update. Then test it with JavaScript disabled. The nav is hidden, so the links are gone too. If those links matter, render them visible and let the script hide them, so the page works either way.

Lesson completed