[@layout]: ../docs.layout.md

# Components

Components add reusable elements to Markdown. Define templates in a `.components.md` file, or contribute components from a TypeScript Plugin.

For an interactive site tool, use a [local Plugin](../extend/plugins.guide.md)
and place its Component in a `.page.md` Source. This keeps routing, layouts,
access-filtered data and live updates together. A standalone `.html` file is served
verbatim: it does not receive Markdown link rewriting, Source layout, or Component
validation. Local entrypoints such as `plugins/tool.plugin.ts` and
`plugins/tool/index.plugin.ts` are discovered beside the project's `paths/` folder.

```md live
<x-navigation-toc max-depth="2" />
```

<x-navigation-toc max-depth="2" />

---

## Markdown components

Markdown components draw on [literate programming](https://en.wikipedia.org/wiki/Literate_programming):
write a document that explains the component, with working code interleaved in
the order that helps readers understand it. Describe what a component does, define its markup,
and explain its styling or behavior alongside the relevant code fences. The
file serves as both an implementation and a guide to using it.

A `*.components.md` file holds one Block per component. PathMX uses the Block's
`html`, `css`, and `js` fences as its implementation. Prose and examples make the
component file readable as a page.

Create `paths/cards.components.md`. This lesson card explains its slots before
defining the template:

````md
<!-- componentName: lesson-card -->

# Lesson card

Group a lesson title and its instructions in a card. The named `title` slot
holds the heading; the default slot holds the lesson content.

```html
<article class="lesson-card">
  <header><slot name="title" /></header>
  <slot />
</article>
```

A border separates the card from the surrounding page. Use the theme's border
color so the card follows the site's appearance.

```css
.lesson-card { border: 1px solid var(--pmx-color-border); }
```
````

Use `md` fences for authoring examples within a component Block. An example
written as an `html` fence becomes part of that component's template.

The [reading shelf components](../../examples/bookcase/shelf.components.md)
show this style with explanations and a working two-row bookcase.

Create `paths/lesson.page.md`, import the file, and use the tag:

```md
[@cards]: ./cards.components.md

# Your first lesson

<lesson-card>
  <slot name="title">Week 1</slot>
  Set up the toolchain.
</lesson-card>
```

Run `bun run dev` and open `/lesson.page`. The card groups “Week 1” and its
instructions inside a border. Edit the component's padding or the lesson text to
see the result update.

### Wrap saved-response controls

When a literate component presents a response field, put the
[Input controls](./input.reference.md) in the authored slot. They retain the
containing Source's response Block and stable field names.

For example, define this component in `cards.components.md`:

````md
---

<!-- componentName: rating-field -->

## Rating field

```html
<fieldset>
  <legend>{{ label }}</legend>
  <slot />
</fieldset>
```
````

Then use it in a response Block:

```md
<!-- id: review -->

<rating-field label="Reading comfort">
<x-input-radio name="comfort" value="comfortable" label="Comfortable" required />
<x-input-radio name="comfort" value="difficult" label="Difficult" />
</rating-field>

<x-input-submit label="Save review" update-label="Update review" />
```

Saving uses the same Actor, writable Home, and response rules as ordinary Input.
Keep the controls in the slot rather than generating them inside the literate
template with interpolated names such as `name="{{ name }}"`; that form is not
supported and produces a Plugin-owned row-consumer diagnostic. Use a Plugin
component for dynamic field generation through its supported compile contract.

| Piece                        | Meaning                                                              |
| ---------------------------- | -------------------------------------------------------------------- |
| `<!-- componentName: x -->`  | Block topmatter naming the tag.                                      |
| `html`, `css`, `js` fences   | Template, global stylesheet in `pmx.plugins`, and browser script.                     |
| `<slot />`                  | Where ordinary children render.                                      |
| `<slot name="x" />`          | Named region filled by `<slot name="x">…</slot>` in the use.         |
| `{{ href }}`, `{{ title }}`  | In a query `component`, each result's route, meta, and data fields.  |
| `[@alias]: ./x.components.md`| Imports a component file; the alias is local.                        |

---

## Fragment links and authoring diagnostics

A component template's `href="#top"` stays on the page using the component when
`top` is outside the template. The same applies to SVG `<use href="#page-sprite">`.
IDs declared inside the template and their local references are scoped to each
instance. To link to the definition's own document, write its path explicitly,
for example `href="./cards.components.md#details"`. Included content keeps its
Source-relative fragment behavior.

For example, a page with `<div id="top">` can use a component containing
`<a href="#top">Back to top</a>` without navigating to the component's file.
An included excerpt's `#details` still points at its original Source when that
heading is outside the excerpt.

Run `bun run pmx lint --format json` to locate a known hyphenated component used
without its Source-local import. Importing it in a layout makes it available only
to the layout markup. A component document that demonstrates its own tags also
needs an import of itself. Inline code, fenced examples and comments are ignored;
unknown browser custom elements are not classified as missing PathMX components.

Lint reports duplicate component definitions at both Sources while `pmx mv` and
`pmx rm`, including `--dry-run`, remain available to repair the project. A
`componentName` declaration must be the Block's leading metadata comment, before
link definitions; lint warns when the declaration is hidden behind those links.

---

## Contract

Plugin components use `x-<plugin>-<name>` tags. Core ships these:

| Tag                                          | Renders                                            |
| -------------------------------------------- | -------------------------------------------------- |
| `<x-navigation-nav name="primary" />`        | A named `.navigation.md` menu, viewer-aware.       |
| `<x-breadcrumbs-trail />`                    | The folder trail to the current Source.            |
| `<x-navigation-toc max-depth="3" />`  | Headings of the current page. `min-depth` too.     |
| `<x-theme-scheme-toggle />`                  | Light and dark scheme switch.                      |
| `<x-source-content />`                            | Layout outlet for the page body (Layouts only).    |
| `<x-source-embed src="./example.page.md" />` | Independent page embedded inside this Source. |
| `<x-source-references />` | Layout destination for independently open references. |
| `<x-source-overlay />` | Layout declaration for Source overlays. |

[Sound effects](./sfx.reference) provides `x-sfx-cue`, `x-sfx-loop`,
`x-sfx-toggle`, and `x-sfx-volume` for authored audio and viewer controls.

Packages add their own, such as `<x-search-trigger />`, `<x-player-app />`, `<x-completion-progress />`, and `<x-auth-session />`.

---

## Source embeds

Embed another Source's complete standalone page, including its layout:

```html
<x-source-embed src="./example.page.md" />
```

The stock `SourcesPlugin` supplies this component. `src` resolves beside the
Source that authors the component and accepts a local Source filename or canonical
route, with optional query parameters and a fragment. Unreadable targets render
as unavailable. Remote websites and non-page Sources are not supported.

Unlike `@include`, the embed is an independent iframe with its own styles,
scrolling, and interactions. Internal links navigate within the frame. External
links follow the site's Embedding plugin policy. Without JavaScript, the Source's
title remains a normal link to its page. Repeated ancestor pages and nesting beyond
four embed levels also fall back to that link.

Every PathMX document marks its root `<html>` with `data-pmx-embedded` when
the browser loads it inside a frame. The marker is set before page styles and
client hydration, including when the Embedding plugin is absent. Sources and
Plugins can use it to adapt their presentation:

```css
html[data-pmx-embedded] .standalone-tools { display: none; }
```

The local repository-admin switcher uses this marker to hide inside frames.
It describes the browser presentation, not permissions or a different layout;
server rendering has no frame context. With JavaScript disabled, the browser
cannot set the marker.

`id` and `class` identify and style the host. Set `--pmx-source-embed-height` in
an authored stylesheet to change its viewport height; the default is the smaller
of `32rem` and `75svh`. The iframe fills the available width and uses the Source's
title as its accessible name. Its height does not automatically expand to fit
its entire document.

```html
<x-source-embed id="working-example" src="./example.page.md" />

<style>
  #working-example { --pmx-source-embed-height: 36rem; }
</style>
```

The embedded document survives containing-page live edits when the embed host
and destination remain stable. Changing `src` loads the new destination.
Try the [independent Source example](../../examples/source-embed.demo.md). For managed
references and overlays, see [Opening Sources](./source-opening.guide.md).

Player-enabled destinations can start in Slides or Focus using the same URL
parameters as ordinary Player links:

```html
<x-source-embed src="./lesson.page.md?play=presentation#practice" />
<x-source-embed src="./lesson.page.md?play=focus" />
```

The destination needs `<x-player-app />` in its page or layout and the Player
plugin installed. Player stays within the iframe viewport; its position and mode
do not change the containing page's URL. See [embedded slides](../../examples/source-embed.demo.md#embedded-slides).

---

## Table of contents

Navigation provides `<x-navigation-toc />` to list the current page's headings. Use `min-depth`
and `max-depth` to choose heading levels, and `title` to change “On this page.”
The route follows heading nesting and highlights the current section as you read.
Headings inside Source Blocks appear in document order.

The outline defaults to a disclosure showing the current section. Choosing a
heading closes it. When your layout places it beside the article, set
`--pmx-toc-layout: sidebar` on its wrapper to keep it expanded. Switch to
`inline` in the same rule that moves it above the article:

```css
.reading-layout {
  container: reading / inline-size;
}
.reading-outline {
  --pmx-toc-layout: sidebar;
}
@container reading (max-width: 60rem) {
  .reading-outline {
    --pmx-toc-layout: inline;
  }
}
```

Choose the container threshold to match your layout's column rules. The component
follows that layout signal instead of a viewport breakpoint. Resizing within
one mode preserves the reader's open or closed choice. Allow for any sticky
navigation in the page's scroll padding.

Anchor links scroll smoothly with the default theme. Reduced-motion preferences
use immediate scrolling and route updates. Without JavaScript, the outline stays
expanded and its links still work.

---

## Component data

Use the [complete artist/type/site layout example](../site/layouts.reference.md#nest-a-type-layout-in-a-site-layout)
for the Source dataset, component file, and layout wiring. A template can read the represented Source and the containing Block without a
binding declaration. Explicit component attributes remain ordinary placeholders:

````md
<!-- componentName: artist-summary -->

# Artist summary

```html
<header>
  <h1>{{ source.title }}</h1>
  <p>{{ source.category }}</p>
  <p>Visit booth {{ source.booth }}</p>
  <span>{{ block.caption }}</span>
  <span>{{ label }}</span>
</header>
```
````

Import the file from an artist layout and use `<artist-summary label="Exhibitor" />`.
`source.title`, `source.category`, and `source.booth` come from the artist being
presented. The layout Source and the component definition do not replace that
Source. `block.caption` belongs to the Block that authors this use; it is empty
when the use has no containing Block or that Block has no caption.

| Scope | Values |
| --- | --- |
| `source.<key>` | Represented Source data, canonical `id`, `type`, `href`, and derived `title`, `description`, `image`. |
| `block.<key>` | Containing or represented Block data, canonical `id`, `type`, `href`, and its own derived metadata. Source data is not inherited. |
| `{{ label }}` | Explicit `label` attribute. Query results and `component:` self-rendering retain their existing flat field mapping. |

Canonical identity and derived metadata take precedence over colliding authored
fields. Nested data uses dot notation, such as `source.contact.website`. Scalar
values are HTML-escaped; absent values and non-scalar objects render empty. This
is field interpolation, without loops or expressions. Use a Plugin component for
iteration or domain-specific rendering. Ordinary Markdown retains unresolved
tokens as authored text and also supports `source.<key>` and `block.<key>` from
its own compile-time Source and Block.

Nested components inherit the represented context, while authored slots retain
their original containing Block. In Source query results and rich references,
`source` is the result or target Source. In Block results, `block` is that result
Block and `source` is its owner. A custom named query can return a canonical
`source` reference to select that context; its other scalar fields remain flat
attributes.

Scoped templates read through the current View when rendering, so admission rules
and live Source dependencies apply. Values are not embedded into shared compiled
templates or automatically sent to browser JavaScript. Actor and application
state are not implicit template scopes. Browser scripts receive explicit instance
attributes. Use [View bindings](./queries.reference.md#view-bindings) such as
`$profile.display-name` for declared Source data and typed query projections, and
Plugin components for Actions. Bindings can appear directly in a template or in
an instance's scalar attributes; they resolve for each current View.

---

## Browser lifecycle

Add a second Block to the same `paths/cards.components.md`:

````md
---

<!-- componentName: lesson-counter -->

## Lesson counter

```html
<div>
  <button type="button">Practice again</button>
  <output>0</output>
</div>
```

```js
let count = 0
on("button", "click", () => {
  count += 1
  $("output").value = String(count)
})
```
````

Add `<lesson-counter />` to `paths/lesson.page.md` after its existing import.
Click Practice again: the output increments. This count is local interaction
state, so remounting or reloading resets it.

Keep related controls inside one HTML root so the instance's scoped helpers can
reach them. A component's `js` fence runs when that instance mounts. Use `on()` to register
event listeners and `cleanup()` to release resources when the instance is removed
or its script changes. PathMX removes listeners registered with `on()` automatically.

The JavaScript fence receives these helpers:

| Helper | Behavior |
| --- | --- |
| `el` | This instance's root element. |
| `$(selector)` | First matching descendant, or `null`. |
| `$$(selector)` | Matching descendants as an array. |
| `on(target, event, handler, options?)` | Listen on a descendant selector or an `EventTarget`, including `document`; listeners are removed on cleanup. |
| `attrs` | Current instance attributes; refreshed before update callbacks. |
| `update(fn)` | Run immediately and again when this instance's markup changes. |
| `cleanup(fn)` | Release a resource when the instance is disposed. |
| `input` | Access the instance's compiled Input field, when declared. |
| `settings` | `get(plugin)` reads and `set(plugin, values)` saves that Plugin's [device settings](../extend/state.reference.md#add-device-settings); both return Promises and require installed Settings. |

A `css` fence keeps its selectors unchanged and applies page-wide in
`@layer pmx.plugins`. Prefix selectors with your component's root class to limit
where they apply. Normal unlayered site styles override normal layered component
styles regardless of specificity; see [Cascade layers](../site/styles.reference.md#cascade-layers).
For a link styled as a button, use `<a class="pmx-button" data-pmx-link="button" href="…">…</a>`
and exclude `[data-pmx-link]` from broad site link rules.

Repeated presentation components can share a `name`, including controls in a radio
group. Each instance gets distinct generated IDs for its local labels, ARIA
references, and SVG paint references. Treat generated IDs as internal; use an
authored class or data attribute to select your component.

### Updating the page after an interaction

When someone clicks a button or makes a selection, update the existing HTML
elements that need to change. Keep the surrounding elements in place. This helps
the browser keep keyboard focus, continue playing audio or video, and display
images and animations without interruption.

The counter above follows this pattern:

```js
$("output").value = String(count)
```

Here, `$("output")` finds the existing `<output>` inside this copy of the
component. Assigning its `value` changes the displayed count. The button stays
in place, so someone using a keyboard can press it again without moving focus.

For more complex components, create the fixed structure once and update it as
people interact. A chessboard, for example, can keep its 64 square elements and
the images of pieces that have not changed. Commentary can stay in place until
there is a different note to show.

Assigning `innerHTML` or calling `replaceChildren()` on a container removes its
existing children. Use these when replacing that content is intentional; avoid
rebuilding an entire interactive component just to change one value or item.
PathMX does not automatically preserve elements that your JavaScript removes.

To check an interaction, try it with both a pointer and a keyboard. Confirm that
focus stays where expected and that unchanged images, media, and controls remain
in place.

### Keeping local state when page content changes

Live updates can replace a component's rendered contents. By default, an affected
instance cleans up and mounts again. To keep local state across updates, register
an `update(fn)` callback: it runs immediately and when the component's rendered
markup changes, with `attrs` refreshed. During live and Action updates, unchanged
rendered subtrees retain their DOM and local state, including components in layouts
outside Blocks. Updating a Graph footer, for example, does not reset an unchanged
lesson or its widgets. Navigation still reconciles the destination's instances.
Query current elements inside the callback and restore any generated content there.

During `pmx dev`, edits to separate `.js` or `.mjs` files under the Source
root reload connected pages, including all nested module imports. This also
resets component state; `update()` preserves state across document morphs,
not full page reloads. Editing a component's inline `js` fence still uses
the cleanup-and-mount lifecycle above.

---

## Browser module lifecycle

For a complete site-wide bundle setup, follow
[Add a browser module](../extend/plugins.guide.md#add-a-browser-module). Put the
TypeScript below in that example's `plugins/counter/client.ts` and the HTML in
`paths/counter.page.md`.

Choose who owns the HTML before adding behavior. Use server-rendered HTML with
`mountPathMXBehavior` when the server should replace form structure as data or
navigation changes. A Plugin component with `client: moduleRef(...)` owns its
interactive root: its client must update the contents when new data arrives.
Returning different server HTML alongside `clientData` does not transfer that
ownership back to the server. See [Plugin components](../extend/plugins.reference.md#components).

Browser entrypoints can attach behavior to ordinary HTML with
`mountPathMXBehavior` from `@pathmx/core/controls/browser`. It mounts matching
roots immediately, retains each root's local state, and handles later PathMX
renders and removal. For example, this counter keeps its count when authored
HTML is refreshed:

```html
<div data-counter>
  <button type="button">Count</button>
  <output>0</output>
</div>
```

```ts
import { mountPathMXBehavior } from "@pathmx/core/controls/browser"

const stop = mountPathMXBehavior("[data-counter]", (root, { signal }) => {
  let count = 0
  const update = () => {
    const output = root.querySelector("output")
    if (output) output.value = String(count)
  }
  root.addEventListener("click", (event) => {
    if (!(event.target instanceof Element) || !event.target.closest("button")) return
    count += 1
    update()
  }, { signal })
  return { update }
})

// Call stop() if the entrypoint itself is being disposed.
```

Core loads newly required browser bundle entrypoints before committing an
incoming document. Use this lifecycle helper so arriving roots mount at commit;
module evaluation may still see the previous document. Shared component client
entrypoints use one bundle URL regardless of which component a page authors first.

Setup is synchronous and runs once per matching root. Return an object with
optional `update()` and `dispose()` methods, or return nothing for listeners
that need no additional lifecycle work. `update()` runs once after setup and
after relevant renders. During live and Action updates, an unchanged rendered
subtree skips updates, whether it is inside a Block or in the surrounding layout.
Changed server attributes and content still apply; unchanged attributes retain
browser-owned state. Query current children inside `update()` because a retained
root's authored contents may have changed. If a behavior depends on data outside
its root, subscribe to that data explicitly rather than using unrelated page
renders as its update signal.

New matching roots mount on the next PathMX render. Removed roots dispose at
the next DOM mutation checkpoint, even without another render. A root that no
longer matches the selector disposes during reconciliation. Disposal first
aborts `signal`, removing listeners registered with it, and then calls
`dispose()` once. The returned `stop()` function stops reconciliation and
disposes all remaining roots.

For lazy imports, check the signal after loading and release any mounted
resource in `dispose()`. In this example the local chart module exports
`mountChart(root)`, returning `update()` and `dispose()` methods:

```ts
import { mountPathMXBehavior } from "@pathmx/core/controls/browser"

mountPathMXBehavior("[data-chart]", (root, { signal }) => {
  let chart: { update(): void; dispose(): void } | undefined
  void import("./chart.js").then(({ mountChart }) => {
    if (signal.aborted) return
    chart = mountChart(root)
  }).catch((error) => {
    if (!signal.aborted) console.error("Chart failed to load.", error)
  })
  return {
    update() { chart?.update() },
    dispose() { chart?.dispose() },
  }
})
```

React's `mountPathMXApps` uses the same lifecycle for root retention, updates,
and unmounting. See the [React reference](../extend/react.reference.md).

---

## Plugin components

Start with the complete [reading-time Plugin](../extend/plugins.guide.md#create-the-component)
for dependencies, named files, activation, and rendered output.

TypeScript Plugins declare native components with `defineComponent`.
Authored preparation lives in `compile`; `load` reads the current View, and the
portable runtime presents those inputs. Query rows invoke the reusable compiled
component with structured props instead of recompiling generated Markdown.

`owner` identifies the authoring Source. `page` identifies the displayed Source,
including its instance. Read viewer-specific state through the admitted `view`.
See the [Plugin API](../extend/plugins.reference.md#compiled-components-and-block-outputs)
for the complete contract and ordinary Block outputs.

---

## Forms in components

Native inputs, selects, and textareas in a component receive the site's shared
field styles. Use `.pmx-field` to group a label with its control and supporting
text, and `.pmx-button` for submit actions. Keep component CSS focused on layout
and intentional variations; use the existing theme tokens for colors and shapes.

Follow the [Form fields](../site/ui.reference.md#form-fields) conventions
for labels, errors, disabled states, and mobile sizing. CSS supplies presentation;
your form or plugin owns submission, validation messages, and saved values.
[Actions](../extend/actions.reference.md) describe how native forms invoke
registered operations.


---

## Related

- [Queries](./queries.reference.md) · [Callouts](./callouts.reference.md)
- Real component files: [docs components](../docs.components.md), [play layout](../../examples/play-layout.components.md)
- [Reuse content](./reuse-content.guide.md)


---

<!-- id: client-input-fields -->
## Custom Input clients

Components that declare an Input field receive an `input` capability in their
literate JavaScript fence. The compiled binding supplies response and field ownership.
The field name must match the component's declared field; it is not a global lookup.

```js
const field = input.field(attrs.name)
const editor = $("input")
function render() {
  const { value, editable } = field.getSnapshot()
  const text = String(value ?? "")
  if (editor.value !== text) editor.value = text
  editor.disabled = !editable
}
cleanup(field.subscribe(render))
render()
on(editor, "input", () => {
  const text = editor.value
  field.set(text === "" ? undefined : Number.isFinite(Number(text)) ? Number(text) : text)
})
```

This numeric editor retains incomplete text in the shared draft. The Input model
validates it; invalid values do not become valid output or bypass Submit validation.
An ordinary Input Submit control saves the response. Subscriptions do not fire
immediately: subscribe, then perform the initial render. Dispose subscriptions with
`cleanup`; disposal releases the component view without erasing response state.

Native clients can obtain the same scoped capability with
`inputFor(componentRoot)` from `@pathmx/core/input/browser`. The root must carry its
compiled Input declaration. This capability reaches only the fields declared by
this editor, not every field in the response; `input.field("another-field")` does
not expand its scope. Neither DOM proximity to a form nor a field name alone
establishes a binding. Handles become non-editable and reject writes after their
component is removed or rebound. [React clients](../extend/react.reference#input-fields) use
`useInputField` over the same response owner.


---

<!-- id: multi-field-editors -->

## Multi-field editors

One editor can coordinate several existing Input fields without a second hidden
component or a separate draft store. A signature editor, for example, saves a name
and either a typed style or drawn mark while consent stays an ordinary checkbox.
Declare the string props that name its fields:

```ts
const signature = defineComponent({
  tag: "x-offer-signature",
  props: z.object({ name: z.string(), mark: z.string() }),
  input: ["name", "mark"],
  render({ props }) {
    return {
      html: signatureEditorMarkup,
      clientData: { name: props.name, mark: props.mark },
    }
  },
  client: moduleRef(() => import("./signature.client.ts")),
})
```

Here `signatureEditorMarkup` is the Plugin's editor UI. The authored invocation
selects fields already declared in its owning response Block:

```html
<x-offer-signature name="signature" mark="signature-mark" />
<x-input-checkbox name="accept" value="yes" label="I agree" required />
<x-input-submit label="Sign" />
```

The `input` array lists **prop keys**, not literal field names. Core checks that
each prop has a string value naming a field in this Block. It does not create
fields or expose the rest of the form. Keep the existing field declarations and
any native fallback controls; the component changes how those values are edited.
`input: true` is the single-field form using the required string `name` prop.

In the client, obtain the declared handles:

```ts
import { inputFor } from "@pathmx/core/input/browser"

const input = inputFor(element)
const signature = input.field(data.name)
const mark = input.field(data.mark)

// In the editor's adopt handler:
signature.set(adopted.name)
mark.set(adopted.style === "drawn"
  ? `draw:${adopted.path}`
  : `type:${adopted.style}`)
```

Subscribe to both handles and render their current snapshots on mount and after
changes; dispose both subscriptions with the client. Ordinary Submit validates
and saves the whole response. The two `set` calls update the same draft; they are
not a new atomic transaction API. Consent is independently required and cannot be
changed through this editor's capability. Multi-field clients use named handles;
Core does not choose a first field for `context.field`.
