Client-Side Scripts

By Syncfusion®, Inc. · Last updated

Script tags in Astro components, how scripts are bundled and run once, importing npm packages, TypeScript in scripts, is:inline and define:vars, event handling in repeated components, data attributes, web components, scripts from public/ and Partytown.

Client-Side Scripts

  • So far, every page has been plain HTML. Nothing reacts to clicks.
  • A client-side script is JavaScript that runs in the visitor's browser.
  • In Astro, you add one with a normal <script> tag in a component.
  • In this lesson, you will add small interactive features to Asha's site without any UI framework.

Simple lesson: a <script> tag adds JavaScript to the page. Use it only for parts that react.

A Real-World Example

  • Asha's readers often study at night. They ask for a dark theme.
  • The theme must switch when a reader clicks a button.
  • The page must remember the choice for the next visit.
  • That needs JavaScript in the browser, so Asha adds a small <script> to her ThemeToggle component.

Press ▶ Run, then click the button. Open HTML sent to browser to find the script.

What happened:

  • The heading and paragraph are plain HTML. They need no JavaScript.
  • Only the button got a script.
  • The script sets data-theme on the <html> tag. The global CSS reacts to it.
  • localStorage remembers the choice. On a real site, it lasts between visits.

What You Will Learn

In this lesson, you will learn:

  1. How Astro processes a <script> tag.
  2. Why a script runs only once, even in a repeated component.
  3. How to import npm packages and write TypeScript in scripts.
  4. When to use is:inline and define:vars.
  5. How to handle events for many copies of a component.
  6. How to pass data from the component script to the browser.
  7. How web components (custom elements) fit in.
  8. How to load scripts from public/ and third parties.

Script Tags

script in Astro Components

  • Write <script> anywhere in a component. Most people put it at the end.
  • It runs in the browser, after the page loads.
  • It cannot see the variables from the component script. That code ran at build time.

Bundling Scripts

  • Processed script = a plain <script> tag. Astro processes it at build time.
  • Astro:

    • removes TypeScript types,
    • joins imports into one file,
    • shrinks the code,
    • adds type="module" to the tag.
  • type="module" scripts run after the HTML is ready, so the elements already exist.

Importing npm Packages in Scripts

  • A processed script can import packages you installed with npm:
<script>
  import confetti from "canvas-confetti";
  document.querySelector("#party")?.addEventListener("click", () => confetti());
</script>
  • Astro bundles the package into your script. The visitor downloads only what you used.

Scripts Run Only Once

  • If a component appears 10 times on a page, its <script> is added once, not 10 times.
  • So the script must find every copy of the component itself, with querySelectorAll().
  • You will do this in Handling Events in Repeated Components below.

This is true only for processed scripts. A script with is:inline or define:vars is repeated once for every copy of the component.

Animation: three LikeButton copies on a page share one script. The script reaches out to all three buttons.

The page

♥ Like ♥ Like ♥ Like

1 script querySelectorAll()

Three copies of the component, one script. The script must find all three.

[JS] Writing Scripts in JavaScript

  • Write normal browser JavaScript inside the tag.
  • document.querySelector(), addEventListener() and localStorage all work as usual.

[TS] Writing Scripts in TypeScript

  • A processed <script> can hold TypeScript. Astro removes the types.
  • Tell TypeScript what an element is: document.querySelector<HTMLButtonElement>("#save") or as HTMLButtonElement.
  • astro check checks scripts too.

Simple lesson: a plain <script> is bundled, typed, shrunk and added once per page.

Inline Scripts

is:inline

  • <script is:inline> tells Astro: "do not process this script".
  • Astro puts it into the HTML exactly as written:

    • no bundling, no TypeScript, no imports,
    • and it is repeated for every copy of the component.
  • Use it for small scripts that must run before the page is drawn, such as setting the theme early.

define:vars with Scripts

  • define:vars passes values from the component script into a browser script.
  • <script define:vars={{ greeting }}> lets the script use greeting.
  • It makes the script inline: no bundling, and it runs once per component copy.

When to Skip Processing

Use is:inline when:

  • the script must run as early as possible, before CSS and images,
  • the script is a small third-party snippet that must stay exactly as given.

Otherwise, use a plain <script>. You get bundling, TypeScript and smaller files.

Press ▶ Run. Compare the three scripts in HTML sent to browser and in the Terminal.

  • The processed script became <script type="module">, with the types removed.
  • The inline script is unchanged.
  • The define:vars script got greeting from the build.

Simple lesson: use a plain <script> by default. Use is:inline only for early or third-party snippets.

Event Handling

Selecting Elements

  • document.querySelector("#id") finds the first match.
  • document.querySelectorAll(".class") finds every match.
  • Give the elements you need a clear id, class or data- attribute.

Adding Event Listeners

  • element.addEventListener("click", () => { ... }) runs code on each click.
  • Other common events: "input", "submit", "change", "keydown".

Handling Events in Repeated Components

  • The script runs once, so loop over every copy.
  • Inside each copy, find that copy's own elements. Then each button changes only its own counter.
  • Click each button. Each one counts on its own.
  • HTML sent to browser has three buttons but only one script.

Simple lesson: loop with querySelectorAll(), and work inside each copy.

Passing Data to Scripts

Data Attributes

  • Data attribute = an attribute that starts with data-, such as data-post-id="42".
  • You set it from the component script: <button data-message={message}>.
  • It is the safe, simple bridge from build time to the browser.

Reading Data in Scripts

  • In the script, read it with element.dataset.
  • data-post-id becomes dataset.postId. Dashes turn into camelCase.
  • Every value arrives as a string. Use Number() for numbers.

Simple lesson: put values in data- attributes at build time. Read them with dataset in the browser.

Web Components

Custom Elements in Astro Components

  • Web component = your own HTML tag, such as <reading-timer>, with its own behaviour. Browsers support them without any framework.
  • Custom element = the JavaScript class behind the tag. You register it with customElements.define().
  • The tag name must contain a dash: reading-timer, not readingtimer.

connectedCallback

  • connectedCallback() runs each time a copy of the tag appears on the page.
  • So each copy sets itself up. There is no need for querySelectorAll().

Passing Data to Custom Elements

  • Use data- attributes again. Inside the class, read this.dataset.

Simple lesson: a custom element sets up each copy by itself in connectedCallback().

External Scripts

Loading Scripts from public/

  • A script file in public/ is not processed.
  • Load it with <script is:inline src="/analytics.js"></script>.
  • Use this for files that must keep their exact name and content.

Third-Party Scripts with Partytown

  • Third-party script = code from another company, such as analytics or ads.
  • These scripts can slow your page, because they run on the browser's main thread.
  • Partytown = an optional integration that moves them to a web worker, a background thread.
  • Add it with npx astro add partytown. Then mark the script: <script type="text/partytown" src="...">.

Every script costs the visitor download and run time. Before adding one, ask: "Does this part really need to react in the browser?"

When many parts of a page need complex state, a UI framework island may be easier than plain scripts. You will learn that in Astro Islands.

Simple lesson: load public/ scripts with is:inline src. Move heavy third-party scripts off the main thread with Partytown.

TL;DR

  1. Client-side script = JavaScript that runs in the browser. Add it with <script>.
  2. Processed script = plain <script>. Bundled, TypeScript allowed, npm imports allowed, type="module", added once per page.
  3. is:inline = not processed. Copied as written, once per component copy.
  4. define:vars on a script = passes build values in. Makes the script inline.
  5. Repeated components = querySelectorAll() and work inside each copy.
  6. Data attribute = data-x={value} at build time, element.dataset.x in the browser.
  7. Web component = customElements.define("my-tag", Class). connectedCallback() runs for each copy.
  8. public/ scripts = <script is:inline src="/file.js">.
  9. Partytown = runs third-party scripts in a web worker.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Using a component-script variable in <script>Both scripts share variablesPass it with a data- attribute or define:varsReferenceError in the browser
querySelector in a repeated componentThe script runs once per copyLoop with querySelectorAll()Only the first copy works
TypeScript in is:inlineEvery script is processedOnly plain <script> tags are processedSyntax error in the browser
import in is:inlineImports always workUse a plain <script> for importsThe browser cannot find the package
Reading dataset.likes as a numberData attributes keep their typeUse Number(...)"12" + 1 becomes "121"
Custom element name without a dashAny name worksUse a dash: reading-timecustomElements.define throws an error

Check Yourself

Exercises

What to Remember

  • A plain <script> runs in the browser. Astro bundles it and adds it once per page.
  • Loop with querySelectorAll() for repeated components.
  • Pass build-time values with data- attributes or define:vars.
  • Use is:inline only for early or third-party snippets.
  • Web components set up each copy in connectedCallback().

Checkpoint: Asha's site now has a layout, a menu, many pages, styles and a theme toggle. That is a complete static site.

Next: Markdown and MDX, where Asha writes her posts as text files instead of code.