diff --git a/docs/guides/javascript/react/reactautoinit.md b/docs/guides/javascript/react/reactautoinit.md
new file mode 100644
index 0000000000..295206d9fa
--- /dev/null
+++ b/docs/guides/javascript/react/reactautoinit.md
@@ -0,0 +1,218 @@
+---
+title: Mustache Helper and Autoinit
+tags:
+ - react
+ - javascript
+description: How Moodle's Mustache React helper and react_autoinit work together to render, mount, and manage React components.
+---
+
+import { Since } from '@site/src/components';
+
+
+
+This page explains the combined developer contract between `mustache_react_helper` and `react_autoinit`, including how template JSON is converted into markup, how modules are resolved, and how components mount and unmount.
+
+## Purpose
+
+Use this integration when UI is produced by Mustache or fragment HTML and React components should mount automatically without manual bootstrap code.
+
+Source files:
+
+- `public/lib/classes/output/mustache_react_helper.php`
+- `public/lib/js/esm/src/react_autoinit.ts`
+
+## End-to-end flow
+
+1. You write a `{{#react}} ... {{/react}}` block in a Mustache template.
+2. `mustache_react_helper` converts the JSON config into a `
` with `data-react-component` and `data-react-props` attributes.
+3. `react_autoinit` finds the element, imports the module via the browser import map, and mounts it.
+4. If the region is replaced later (AJAX/fragments), components are mounted or unmounted automatically.
+
+## Mustache helper (`{{#react}}`)
+
+The `{{#react}}` block accepts a JSON object followed by optional fallback HTML content:
+
+```mustache title="mod/book/templates/view.mustache"
+{{#react}}
+{
+ "component": "@moodle/lms/mod_book/viewer",
+ "props": {
+ "title": "{{title}}",
+ "chapter": "{{chapter}}"
+ },
+ "id": "book-viewer",
+ "class": "book-viewer-wrapper"
+}
+
Loading…
+{{/react}}
+```
+
+### JSON keys
+
+| Key | Required | Maps to |
+|-----|----------|---------|
+| `component` | Yes | `data-react-component` attribute |
+| `props` | No | `data-react-props` attribute (JSON-encoded) |
+| Any other key | No | Regular HTML attribute (`id`, `class`, `aria-*`, etc.) |
+
+### Mustache tags inside the block
+
+The entire block is passed through the Mustache renderer before the JSON is parsed. This means any Mustache tag — including `{{#str}}`, `{{#quote}}`, template variables, and other helpers — can appear inside the JSON values:
+
+```mustache
+{{#react}}
+{
+ "component": "@moodle/lms/mod_book/viewer",
+ "props": {
+ "title": "{{title}}",
+ "confirmLabel": "{{#str}}confirm, core{{/str}}",
+ "cancelLabel": "{{#str}}cancel, core{{/str}}"
+ }
+}
+{{/react}}
+```
+
+The rendered output is a plain string before `mustache_react_helper` attempts JSON parsing, so any valid Mustache syntax is supported.
+
+### Parsing behaviour
+
+- Mustache variables inside the block are rendered before the JSON is parsed.
+- Trailing commas in the JSON object are stripped automatically.
+- If the JSON is invalid but fallback HTML content is present, a plain `
` with the fallback is rendered.
+- If the JSON is invalid and there is no fallback content, an empty string is returned.
+- Invalid JSON is reported via `debugging()` at `DEBUG_DEVELOPER` level.
+
+Boolean attribute values: if a key's value is `true`, the attribute name is emitted without a value. If `false`, the attribute is omitted.
+
+## The DOM contract
+
+### `data-react-component`
+
+The component specifier must be a fully-qualified ESM import specifier in the form:
+
+```text
+@moodle/lms/
/
+```
+
+Examples:
+
+```html
+
+
+```
+
+### `data-react-props`
+
+An optional JSON object passed as the props to the React component:
+
+```html
+
+```
+
+If the value is not valid JSON, `react_autoinit` logs an error and falls back to `{}`.
+
+## How module resolution works
+
+The specifier in `data-react-component` is passed directly to a dynamic `import()` call. The browser resolves it through the Moodle import map, which maps `@moodle/lms//` to the built JS file under the component's `js/esm/build/` directory.
+
+For example, `@moodle/lms/mod_book/viewer` resolves to `mod/book/js/esm/build/viewer.js`.
+
+If the import fails, mounting is skipped and an error is logged to the console.
+
+## Export contract
+
+`react_autoinit` expects a **default-exported React function component**:
+
+```tsx title="mod/book/js/esm/src/viewer.tsx"
+type Props = {
+ title?: string;
+ chapter?: string;
+};
+
+export default function Viewer({title = 'Book', chapter = 'Chapter 1'}: Props) {
+ return (
+
+ );
+}
+```
+
+The component is mounted with `react-dom/client` `createRoot`. If `module.default` is not found, `react_autoinit` logs a warning and skips mounting.
+
+## Lifecycle internals
+
+### Initial run
+
+`react_autoinit` calls `init()` automatically when the bundle loads.
+
+Sequence:
+
+1. Wait for `DOMContentLoaded` (or resolve immediately if the DOM is already ready).
+2. Scan all `[data-react-component]` elements in the document.
+3. Mount each one.
+4. Install a single global `MutationObserver`.
+
+### Mount guard
+
+Each successfully mounted element receives `dataset.reactMounted = "1"`. This prevents duplicate mounting when the same region is rescanned.
+
+### Unmount tracking
+
+The cleanup function returned by `createRoot().unmount` is stored in a `WeakMap void>`. When the element is removed from the DOM, the cleanup function is called automatically.
+
+## Dynamic content (AJAX and fragments)
+
+A `MutationObserver` watches `document.documentElement` with `childList: true` and `subtree: true`.
+
+When content is added:
+
+- If the added node matches `[data-react-component]`, it is mounted.
+- If the added node contains matching descendants, each descendant is mounted.
+
+When content is removed:
+
+- If the removed node matches, it is unmounted.
+- If the removed node contains matching descendants, each descendant is unmounted.
+
+This means React components inside AJAX-loaded fragments or dynamic regions are handled automatically without any additional initializer call.
+
+## Building components
+
+Run the Grunt `react` task from the Moodle root:
+
+```bash
+# Production build — minified, no source maps
+grunt react
+
+# Development build — readable output, inline source maps
+grunt react:dev
+
+# Watch mode — rebuilds changed files automatically
+grunt react:watch
+```
+
+The build tool discovers all `js/esm/src/**/*.{ts,tsx}` files across core and plugins automatically. No registration is required.
+
+## Debugging checklist
+
+If a component does not render:
+
+1. Check that `data-react-component` uses the `@moodle/lms//` format.
+2. Confirm the built file exists under `js/esm/build/`.
+3. Confirm the module has a default-exported function component.
+4. Check the browser console for messages prefixed with `[react_autoinit]`.
+
+If a component mounts multiple times:
+
+1. Ensure the container element is not recreated on every re-render by the surrounding template.
+2. Do not call `createRoot` manually on an element already managed by `react_autoinit`.
+
+## See also
+
+- [Mustache templates](../../templates)
+- [JavaScript modules](../modules)
diff --git a/project-words.txt b/project-words.txt
index b709384c14..ca0a0486d6 100644
--- a/project-words.txt
+++ b/project-words.txt
@@ -360,3 +360,4 @@ modvisible
savechanges
hideif
formslib
+autoinit