# Micra.js — Full LLM Context (llms-full.txt)
This file follows the llmstxt.org "expanded" convention: it inlines code recipes so LLMs that crawl this URL get a complete training surface in one read. The short version is `llms.txt` in the same directory.
> Lightweight reactive TypeScript framework for server-rendered apps and small SaaS frontends. ~5 KB gzip. No build step required.
## Install
```bash
npm install micra.js@^2.3.2
```
```ts
import * as Micra from 'micra.js'
```
Or CDN (no build step):
```html
```
This exposes a global `Micra` object.
> **CDN choice matters for AI sandboxes.** Use `cdn.jsdelivr.net` (allowed by
> Claude artifacts, ChatGPT canvas, and most AI runtime CSPs). Do NOT use
> `unpkg.com` — it is blocked by those Content Security Policies and the script
> will fail to load. jsDelivr auto-mirrors every npm package.
## When to use Micra.js
- Server-rendered pages (Rails, Laravel, Django, Phoenix, etc.) needing a small amount of reactivity
- Bundle-size-sensitive (~5 KB vs 45+ KB React)
- No build step desired — drop a `
```
## Recipe 2 — Todo app with localStorage and filters
```html
Todo
```
## Recipe 3 — Data table with search
```html
Name
Email
Role
No matches.
```
## Recipe 4 — Form with validation and async submit
```html
```
## Recipe 5 — Modal via event bus
```html
```
## Recipe 6 — Tabs (multiple components on one page)
```html
```
## Recipe 10 — Multiple islands on one page (cross-component coordination)
```html
Loading…
No results.
```
## Recipe 11 — htmx bridge (server-driven HTML swaps + Micra islands)
Wire htmx for server-driven DOM swaps and Micra for local reactivity on
the same page. Twelve lines of glue, written once.
```html
```
Rules of thumb:
- **Never put `hx-swap` directly on a `[data-component]` element that
swaps its own `innerHTML`** — the cached directive scan points at gone
DOM. Use a wrapper as the swap target.
- **`Micra.start()` is idempotent**, so re-scanning a subtree that
contains already-mounted siblings is safe.
- **Always scope to `e.target`** in the bridge, not `document` — scanning
the whole page on every swap is wasteful.
Full reference with state-survival patterns, `hx-vals`/`hx-include`
bridging, and per-component loading state lives in
`docs/recipes/htmx.md`.
## Recipe 12 — Rails + Micra (importmap, CSRF, Turbo Drive)
Pin Micra in `config/importmap.rb`, boot it once in the layout — that's
the whole integration. CSRF works automatically (Micra reads
`` that Rails already ships). Turbo Drive needs
a `turbo:load` mirror so the second navigation doesn't ghost.
```ruby
# config/importmap.rb
pin "micra",
to: "https://cdn.jsdelivr.net/npm/micra.js@2.3.2/dist/micra.esm.js",
preload: true
```
```erb
<%# app/views/layouts/application.html.erb — inside %>
<%= csrf_meta_tags %>
<%= javascript_importmap_tags %>
```
```erb
<%# app/views/tasks/index.html.erb %>
```
```js
// app/javascript/application.js
import * as Micra from "micra"
Micra.define("tasks-board", {
state: { tasks: [], draft: "" },
onCreate() { this.state.tasks = JSON.parse(this.prop("initialTasks") || "[]") },
async add() {
const task = await this.fetch("/tasks", { method: "POST", body: { title: this.state.draft.trim() } })
this.state.tasks = [...this.state.tasks, task]
this.state.draft = ""
},
async toggle(e) {
const id = Number(e.currentTarget.closest("li").querySelector("[data-id]").dataset.id)
const next = !this.state.tasks.find(t => t.id === id).done
const task = await this.fetch(`/tasks/${id}`, { method: "PATCH", body: { done: next } })
this.state.tasks = this.state.tasks.map(t => t.id === id ? task : t)
},
async remove(e) {
const id = Number(e.currentTarget.dataset.id)
await this.fetch(`/tasks/${id}`, { method: "DELETE" })
this.state.tasks = this.state.tasks.filter(t => t.id !== id)
},
})
```
Rules of thumb:
- **CSRF is automatic** — keep `<%= csrf_meta_tags %>` in the layout.
`this.fetch()` sends `X-CSRF-Token` on every non-GET request.
- **Add `turbo:load`** alongside `DOMContentLoaded` if you have Turbo
Drive enabled (default since Rails 7). Without it, only the first
page load wires up.
- **JSON in `data-*` attributes does NOT auto-parse.** Non-primitive
props (`@tasks.as_json.to_json`) are strings client-side — call
`JSON.parse(this.prop('initialTasks'))` in `onCreate`. Primitives
(numbers, booleans) auto-cast through `this.prop()`.
- **Turbo Frames** that swap their own `innerHTML` and contain a
`[data-component]` will leave the cached scan pointing at gone DOM.
Same footgun as htmx — keep `data-component` outside the frame, or
destroy+remount on `turbo:frame-render`.
- The optional **`micra-rails`** gem adds an ERB helper
(`micra_component`) and a one-shot installer. Its importmap pin can lag
the latest Micra.js release between gem versions (override it in your
own `config/importmap.rb`), and the `micra_state.to_html` example in
its README doesn't compile — see `docs/recipes/rails.md` §2 for
workarounds.
Full reference with Turbo Streams cleanup, no-flicker hydration pattern,
and the Stimulus-vs-Micra split is in `docs/recipes/rails.md`.
---
# Anti-pattern reference (what LLMs gravitate to — DO NOT)
```js
// ❌ Hand-rolled list rendering — defeats data-each
document.getElementById('list').innerHTML = items.map(...).join('')
// ❌ Derived state as field — diverges from source of truth
state: { todos: [], totalCount: 0, hasDone: false }
updateComputeds() { this.state.totalCount = this.state.todos.length }
// ❌ addEventListener in render — leaks past destroy()
createItem(item) {
const el = document.createElement('div')
el.addEventListener('click', () => this.toggle(item.id))
}
// ❌ Manual re-render after mutation — Micra already does this
addTask() { this.state.todos = [...]; this.renderList() }
// ❌ Nested state mutation — shallow proxy can't see it
this.state.user.email = 'x'
// ❌ Key modifier on @event — not supported
// ❌ Nested path in data-model — writes literal flat key
// ❌ React/Vue/Alpine imports — not Micra
import React, { useState } from 'react'
import { ref, computed } from 'vue'
import Alpine from 'alpinejs'
// ❌ unpkg CDN — blocked by Claude artifacts and most AI sandbox CSPs
// ✅ Use jsDelivr instead — it auto-mirrors npm and is CSP-allowlisted everywhere
```
# Final checklist
Before returning generated Micra code, verify ALL of these:
- [ ] Every list uses `` with `data-key`. No `getElementById`/`innerHTML` for lists.
- [ ] Every derived value (count, total, filtered, formatted) is a **method** on the component, called from a directive via `methodName()`. No state field for things that can be computed.
- [ ] Every event handler is `@event` or `data-on`. No `addEventListener` inside methods.
- [ ] No `this.renderList()` / `this.update()` / `this.refresh()` calls after mutations.
- [ ] State writes are top-level: `state.x = …` or `state.user = { …state.user, … }`. Never `state.user.x = …`.
- [ ] No `@keydown.enter` — branch on `e.key` in the method instead.
- [ ] `Micra.start()` is at the end of the script.
- [ ] No React/Vue/Alpine imports.
- [ ] All timers / external listeners in `onCreate` are cleaned up in `onDestroy`.
- [ ] CDN script src is `cdn.jsdelivr.net/npm/micra.js@…`, never `unpkg.com` (CSP-blocked in Claude/AI sandboxes).
- [ ] Artifact / canvas type is **HTML** (`application/vnd.ant.html` in Claude), NOT React/Next/Vue. Micra cannot run in a React artifact.
# Links
- Full docs: https://denisfl.github.io/micra.js/
- Source: https://github.com/denisfl/micra.js
- npm: https://www.npmjs.com/package/micra.js
- LLM short guide: https://github.com/denisfl/micra.js/blob/master/docs/llm-guide.md
- Recipes directory: https://github.com/denisfl/micra.js/tree/master/docs/recipes
- Short llms.txt: https://github.com/denisfl/micra.js/blob/master/llms.txt