Skip to content

astro

Scripts and event handling

Astro

Scripts and event handling

Scripts and event handling

// Find our component DOM on the page. const buttons = document.querySelectorAll('[data-confetti-button]');

// Add event listeners to fire confetti when a button is clicked. buttons.forEach((button) => { button.addEventListener('click', () => confetti()); });


<ReadMore>See [when your scripts will not be processed](#unprocessed-scripts) to troubleshoot script behavior, or to learn how to opt-out of this processing intentionally.</ReadMore>


## Script processing

By default, Astro processes `<script>` tags that contain no attributes (other than `src`) in the following ways:

- **TypeScript support:** All scripts are TypeScript by default.
- **Import bundling:** Import local files or npm modules, which will be bundled together.
- **Type Module:** Processed scripts become [`type="module"`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) automatically.
- **Deduplication:** If a component that contains a `<script>` is used multiple times on a page, the script will only be included once.
- **Automatic inlining:** If the script is small enough, Astro will inline it directly into the HTML to reduce the number of requests.
```astro title="src/components/Example.astro"
<script>
  // Processed! Bundled! TypeScript!
  // Importing local scripts and from npm packages works.
</script>

Unprocessed scripts

Astro will not process a <script> tag if it has any attribute other than src.

You can add the is:inline directive to intentionally opt out of processing for a script.

<script is:inline>
  // Will be rendered into the HTML exactly as written!
  // Not transformed: no TypeScript and no import resolution by Astro.
  // If used inside a component, this code is duplicated for each instance.
</script>

Include JavaScript files on your page

You may want to write your scripts as separate .js/.ts files or need to reference an external script on another server. You can do this by referencing these in a <script> tag’s src attribute.

Import local scripts

When to use this: when your script lives inside of src/.

Astro will process these scripts according to the script processing rules.

<!-- relative path to script at `src/scripts/local.js` -->
<script src="../scripts/local.js"></script>

<!-- also works for local TypeScript files -->
<script src="./script-with-types.ts"></script>

Load external scripts

When to use this: when your JavaScript file lives inside of public/ or on a CDN.

To load scripts outside of your project's src/ folder, include the is:inline directive. This approach skips the JavaScript processing, bundling, and optimizations that are provided by Astro when you import scripts as described above.

<!-- absolute path to a script at `public/my-script.js` -->
<script is:inline src="/my-script.js"></script>

<!-- full URL to a script on a remote server -->
<script is:inline src="https://my-analytics.com/script.js"></script>

Common script patterns

Handle onclick and other events

Some UI frameworks use custom syntax for event handling like onClick={...} (React/Preact) or @click="..." (Vue). Astro follows standard HTML more closely and does not use custom syntax for events.

Instead, you can use addEventListener in a <script> tag to handle user interactions.

<button class="alert">Click me!</button>

<script>
  // Find all buttons with the `alert` class on the page.
  const buttons = document.querySelectorAll('button.alert');

  // Handle clicks on each button.
  buttons.forEach((button) => {
    button.addEventListener('click', () => {
      alert('Button was clicked!');
    });
  });
</script>

If you have multiple <AlertButton /> components on a page, Astro will not run the script multiple times. Scripts are bundled and only included once per page. Using querySelectorAll ensures that this script attaches the event listener to every button with the alert class found on the page.

Web components with custom elements

You can create your own HTML elements with custom behavior using the Web Components standard. Defining a custom element in a .astro component allows you to build interactive components without needing a UI framework library.

In this example, we define a new <astro-heart> HTML element that tracks how many times you click the heart button and updates the <span> with the latest count.

<!-- Wrap the component elements in our custom element “astro-heart”. -->
<astro-heart>
  <button aria-label="Heart">💜</button> × <span>0</span>
</astro-heart>

<script>
  // Define the behavior for our new type of HTML element.
  class AstroHeart extends HTMLElement {
    connectedCallback() {
      let count = 0;

      const heartButton = this.querySelector("button");
      const countSpan = this.querySelector("span");

      // Each time the button is clicked, update the count.
      if (heartButton && countSpan) {
        heartButton.addEventListener("click", () => {
          count++;
          countSpan.textContent = count.toString();
        });
      }
    }
  }

  // Tell the browser to use our AstroHeart class for <astro-heart> elements.
  customElements.define("astro-heart", AstroHeart);
</script>

There are two advantages to using a custom element here:

  1. Instead of searching the whole page using document.querySelector(), you can use this.querySelector(), which only searches within the current custom element instance. This makes it easier to work with only the children of one component instance at a time.

  2. Although a <script> only runs once, the browser will run our custom element’s connectedCallback() method each time it finds <astro-heart> on the page. This means you can safely write code for one component at a time, even if you intend to use this component multiple times on a page.

You can learn more about custom elements in web.dev’s Reusable Web Components guide and MDN’s introduction to custom elements.

Pass frontmatter variables to scripts

In Astro components, the code in the frontmatter (between the --- fences) runs on the server and is not available in the browser.

To pass server-side variables to client-side scripts, store them in data-* attributes on HTML elements. Scripts can then access these values using the dataset property.

In this example component, a message prop is stored in a data-message attribute, so the custom element can read this.dataset.message and get the value of the prop in the browser.

---
const { message = 'Welcome, world!' } = Astro.props;
---

<!-- Store the message prop as a data attribute. -->
<astro-greet data-message={message}>
  <button>Say hi!</button>
</astro-greet>

<script>
  class AstroGreet extends HTMLElement {
    connectedCallback() {
      // Read the message from the data attribute.
      const message = this.dataset.message;
      const button = this.querySelector('button');
      button?.addEventListener('click', () => {
        alert(message);
      });
    }
  }

  customElements.define('astro-greet', AstroGreet);
</script>

Now we can use our component multiple times and be greeted by a different message for each one.

---

---

<!-- Use the default message: “Welcome, world!” -->
<AstroGreet />

<!-- Use custom messages passed as a props. -->
<AstroGreet message="Lovely day to build components!" />
<AstroGreet message="Glad you made it! 👋" />

:::tip[Did you know?] This is actually what Astro does behind the scenes when you pass props to a component written using a UI framework like React! For components with a client:* directive, Astro creates an <astro-island> custom element with a props attribute that stores your server-side props in the HTML output. :::

Combining scripts and UI Frameworks

Elements rendered by a UI framework may not be available yet when a <script> tag executes. If your script also needs to handle UI framework components, using a custom element is recommended.