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 herThemeTogglecomponent.
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-themeon the<html>tag. The global CSS reacts to it. localStorageremembers the choice. On a real site, it lasts between visits.
What You Will Learn
In this lesson, you will learn:
- How Astro processes a
<script>tag. - Why a script runs only once, even in a repeated component.
- How to import npm packages and write TypeScript in scripts.
- When to use
is:inlineanddefine:vars. - How to handle events for many copies of a component.
- How to pass data from the component script to the browser.
- How web components (custom elements) fit in.
- 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()andlocalStorageall 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")oras HTMLButtonElement. astro checkchecks 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:varspasses values from the component script into a browser script.<script define:vars={{ greeting }}>lets the script usegreeting.- 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:varsscript gotgreetingfrom 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,classordata-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 asdata-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-idbecomesdataset.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, notreadingtimer.
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, readthis.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
- Client-side script = JavaScript that runs in the browser. Add it with
<script>. - Processed script = plain
<script>. Bundled, TypeScript allowed, npm imports allowed,type="module", added once per page. - is:inline = not processed. Copied as written, once per component copy.
- define:vars on a script = passes build values in. Makes the script inline.
- Repeated components =
querySelectorAll()and work inside each copy. - Data attribute =
data-x={value}at build time,element.dataset.xin the browser. - Web component =
customElements.define("my-tag", Class).connectedCallback()runs for each copy. - public/ scripts =
<script is:inline src="/file.js">. - Partytown = runs third-party scripts in a web worker.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
Using a component-script variable in <script> | Both scripts share variables | Pass it with a data- attribute or define:vars | ReferenceError in the browser |
querySelector in a repeated component | The script runs once per copy | Loop with querySelectorAll() | Only the first copy works |
TypeScript in is:inline | Every script is processed | Only plain <script> tags are processed | Syntax error in the browser |
import in is:inline | Imports always work | Use a plain <script> for imports | The browser cannot find the package |
Reading dataset.likes as a number | Data attributes keep their type | Use Number(...) | "12" + 1 becomes "121" |
| Custom element name without a dash | Any name works | Use a dash: reading-time | customElements.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 ordefine:vars. - Use
is:inlineonly 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.