Errors and fixes

Astro: the component stays static because the client directive is missing

In Astro a component without `client:*` renders to HTML and never hydrates. The events stay dead. Pick the directive from when interactivity is needed.

Key points

  • Without `client:*` the component is static HTML: no events, no state.
  • Pick the directive from timing: `client:load` now, `client:idle` later, `client:visible` when it enters the viewport.
  • `client:only` needs the framework name and skips the server render altogether.

Short answer: without a client:* directive, a React, Vue or Svelte component inside Astro renders to HTML and never hydrates. The markup is there, the events are not. Add the directive that matches when interactivity is needed.

The directives

DirectiveWhen it hydratesTypical use
client:loadimmediatelycontrols above the fold
client:idlewhen the browser is idlesecondary interactions
client:visiblewhen it enters the viewportsections at the bottom of the page
client:onlyin the browser onlycanvas, editors, browser APIs

Why the HTML arrives finished

The mechanism is compilation. Astro reads the .astro file at build time and asks the renderer of the framework for an HTML string. The client:* directive is what turns that string into an astro-island element, carrying the component URL, the renderer URL and the serialised props as attributes. Without a directive the node leaves as plain HTML: no script reaches it, so no listener is registered and no state exists. The component is not broken, it is a photograph.

The example

---
import Counter from "../components/Counter.tsx";
---








client:only needs the framework name, because Astro cannot infer it from the file. Without it the build fails.

How to check it

# quante isole contiene davvero l'HTML servito
curl -s http://localhost:4321/ | grep -o "<astro-island" | wc -l

# quali direttive esistono nel sorgente
# (PowerShell: Select-String -Path src/**/*.astro -Pattern "client:")
grep -rn "client:" src --include="*.astro"

The first command counts islands in the served HTML, the second finds directives in the source. When the count is zero on a page that should be interactive, the cause is in the source. When it is far higher than expected, the problem runs the other way and costs bytes: a table with one directive per row turns into a row of hydration points.

What does not cross an island

Props must be serialisable. A function passed from the .astro file to the component does not cross the boundary: the component receives no value and the error appears at runtime. Pass data, or define the handler inside the client component.

When the fix does not work

If you add the directive and the event still does nothing, check the console: a hydration error aborts the island mount. Another case is a component that reads a React context created outside the island. Each island is a separate tree, so the provider has to live inside it.

Watch the cost as well: every island ships the framework runtime. Ten React islands on one page share a single runtime, but you still coordinate ten hydration points. Prefer one island that holds the interaction.

When the usual fix makes things worse

The fix that circulates is to put client:load on everything that does not respond. It removes the symptom and adds two costs. Astro serialises props inside the island attribute: a list of forty rows with one directive per row produces forty islands and forty copies of the data in the document, which the browser reads and hydrates one at a time. The HTML grows, the time before interactivity grows with it, and the original problem was one missing directive.

The limit of client:visible

A known limitation of client:visible: hydration depends on IntersectionObserver. When the island sits inside a closed panel, a container with zero height or an element with display:none, it never intersects the viewport and stays inert. No error appears, the markup is correct and the control does not answer, so the diagnosis looks like a missing directive again and the first wrong guess costs half an hour.

How to prevent it

  • No directive by default: add one only where an event is needed.
  • Prefer client:visible for sections below the fold.
  • Check the HTML for astro-island and for the framework chunk.
  • One island per interactive area: when ten directives are needed for ten rows, the boundary sits in the wrong place.

Where to continue

In the catalogue: hero block, components and blocks. The guides: App Router structure and accessibility checklist; the comparison: how much a UI library weighs.

Frequently asked questions

Why does the button ignore clicks?

The component was rendered to HTML without hydration. Add the matching client:* directive.

Do client:load and client:idle change how the page looks?

No. They change when the JavaScript runs. The initial HTML is the same, except for components that need interactivity for the first paint.

When do I need client:only?

When the component cannot render on the server: it uses the browser, a canvas, or a library that touches window at import time.

Why does client:only need a value?

Because Astro cannot infer the framework from the file in that case. Write client:only="react".

How do I check that the island hydrated?

Look for astro-island in the HTML and for the framework chunk in the Network panel. No chunk means the directive never applied.