Recipe: Astro + Micra.js
Micra fits Astro as a small reactive island — a bit of interactivity on a
content page without pulling a framework for one widget. It’s the slot
@astrojs/alpinejs fills, with a smaller surface. Astro renders the HTML; Micra
hydrates the [data-component] markup in the browser.
Static pages (no view transitions)
Nothing special. Add the script, write the markup, call Micra.start().
---
// src/layouts/Base.astro
---
<html lang="en">
<head>
<script is:inline src="https://cdn.jsdelivr.net/npm/micra.js@2/dist/micra.min.js"></script>
</head>
<body>
<slot />
<script is:inline>
Micra.define('counter', { state: { count: 0 }, inc() { this.state.count++ } })
Micra.start()
</script>
</body>
</html>
<!-- any .astro page -->
<div data-component="counter">
<button @click="inc">+</button>
<strong data-text="count"></strong>
</div>
With view transitions (<ClientRouter />)
When you enable Astro’s client-side routing, navigation swaps the <body>
without a full reload. Two things follow: components on the new page need
mounting, and components on the page you left need tearing down. Micra handles
both — mount on astro:page-load, teardown via Micra.autoCleanup() (v2.7+).
---
// src/layouts/Base.astro
import { ClientRouter } from 'astro:transitions'
---
<html lang="en">
<head>
<ClientRouter />
<script is:inline src="https://cdn.jsdelivr.net/npm/micra.js@2/dist/micra.min.js"></script>
<script is:inline>
// The <head> is not swapped by view transitions, so this runs once.
Micra.define('counter', { state: { count: 0 }, inc() { this.state.count++ } })
Micra.autoCleanup() // destroy components on pages you navigate away from
document.addEventListener('astro:page-load', () => Micra.start()) // mount each page
</script>
</head>
<body>
<slot />
</body>
</html>
astro:page-load fires on the initial load and after every client-side
navigation, so a single listener covers both. Micra.start() is idempotent —
re-scanning is safe; already-mounted roots are skipped. Going the other way,
autoCleanup() watches the DOM and destroys a component when Astro swaps its
element out, running onDestroy and freeing its listeners.
Put Micra.define(...) and autoCleanup() in the <head> script (it isn’t
swapped, so it runs once); keep only Micra.start() on the astro:page-load
event.
Persisted islands
If you keep an island across navigations with transition:persist, its DOM
isn’t removed — so autoCleanup() leaves it alone and its Micra state survives
the navigation. The next astro:page-load calls Micra.start(), which skips it
(already mounted). Persistence just works, no extra wiring.
<aside data-component="sidebar-filter" transition:persist>
<!-- state.query and friends survive page navigations -->
</aside>
When to reach for Astro’s own islands instead
This recipe is for a lightweight island where you’d otherwise ship a whole framework for one widget. If you want full component islands with their own ecosystem — forms, state libraries, a component tree — Astro’s native React/Vue/Svelte islands are the better tool. Micra is the small option, not a replacement for them.