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

# Shared UI

Give readers familiar controls with clear labels, keyboard focus, and predictable
opening and closing behavior.

Native HTML controls use Core's shared styling;
React interfaces can use `@pathmx/ui` for the same appearance and interaction rules.

This page covers working controls and their styling: button variants and tokens,
form fields, touch behavior, dialogs, floating islands, and progress.

---

## Shared controls

Core's Theme Plugin styles opted-in buttons and dialog surfaces. Native HTML and
`@pathmx/ui` React components use the same tokens.

```html
<button class="pmx-button" data-variant="outline" type="button">Continue</button>
<button class="pmx-button" data-variant="ghost" data-shape="icon"
  type="button" aria-label="Zoom in">+</button>
<dialog class="pmx-dialog" aria-label="Connections">…</dialog>
```

Buttons support `data-variant="primary|outline|ghost|danger|floating"` (default:
`primary`), `data-shape="icon"`, and compact `data-size="sm"`. Icon actions need
an accessible name. These classes supply appearance; the owning component supplies
behavior. Use Core's icon catalog for production icon geometry.

| Token | Default | Controls |
| --- | --- | --- |
| `--pmx-control-size` | `2.5rem` | Regular height and icon-action width. |
| `--pmx-control-size-sm` | `2.25rem` | Compact desktop size; touch targets remain at least 44px. |
| `--pmx-control-radius` | `var(--pmx-radius-full)` | Button corners. |
| `--pmx-control-icon-size` | `1.2rem` | Button icon dimensions. |
| `--pmx-control-icon-stroke` | `1.75` | Button icon stroke weight. |
| `--pmx-motion-duration` | `160ms` | Shared state-transition duration. |
| `--pmx-motion-ease` | `ease-out` | Shared transition easing. |

Dialog surfaces share the floating island treatment described below. Focus uses `--pmx-color-focus`; filled actions use
`--pmx-color-on-accent` or `--pmx-color-on-danger` for their text. Reduced-motion
preferences disable control transitions. Set these CSS properties in your theme
stylesheet; no additional frontmatter settings are required.

---

## Form fields

Native text inputs, selects, and textareas receive themed surfaces, borders,
typography, and focus rings without a component wrapper. Use `.pmx-field` to
arrange a label, control, and supporting text together:

```html
<div class="pmx-field">
  <label for="email">Email</label>
  <input id="email" type="email" name="email" autocomplete="email"
    aria-describedby="email-hint" />
  <small id="email-hint">Where should we send your reply?</small>
</div>
```

A separate `<label for="email">` and `<input id="email">` works too. Placeholders
supplement a label; they do not replace it. Inputs keep their native behavior,
including select menus, date pickers, autofill, and textarea resizing.

Use `disabled` for unavailable fields and `readonly` for values that can be
selected and copied but not edited. Mark a known error with `aria-invalid="true"`
and connect its explanation with `aria-describedby`. Put the explanation in
`.pmx-field-error`. Native validation failures use the same border treatment
after user interaction; required fields are not marked invalid on initial load.
Styles do not generate error messages or perform validation.

Checkboxes and radio buttons retain native indicators using the accent color.
Wrap an option and its text in `.input-option` for a full clickable row. Group
related options in `<fieldset class="pmx-fieldset">` with a `<legend>`. Range,
file, color, hidden, and button inputs keep their specialized layout rather
than receiving text-field styling.

The defaults use existing color, font, and radius tokens, work in light and dark
schemes, and remain easy to override with ordinary class selectors. See the
[Form controls example](../../examples/form-controls.demo.md) to try the field
types and states.

---

## Touch and mobile controls

The default Theme Plugin supplies touch behavior for native buttons, links,
inputs, selects, and disclosure summaries, whether or not they use
`.pmx-button`. Custom elements with interactive ARIA roles receive the same
`touch-action: manipulation` default. Repeated taps activate controls without
requesting double-tap zoom; scrolling and pinch-to-zoom remain available.
Ordinary prose keeps the browser's normal gestures.

Prefer native elements for their built-in keyboard and accessibility behavior:

```html
<label class="pmx-field">
  Your name
  <input name="name" autocomplete="name" />
</label>
<details>
  <summary>More options</summary>
  <label class="pmx-field">
    Playback speed
    <select name="speed">
      <option value="1">Normal</option>
      <option value="1.5">1.5×</option>
    </select>
  </label>
</details>
```

Editable native fields default to at least 16px text on devices with a coarse
pointer, avoiding Safari's input-focus zoom. Shared button styles provide
44px minimum touch targets. Custom styles can override these defaults; keep
editable text at least 16px and preserve a usable focus indicator and tap area.

An ARIA role describes a custom control; it does not add keyboard interaction
or state management. Supply those behaviors when native HTML cannot express
the control. Shadow DOM controls should apply `touch-action: manipulation` to
their host or interaction surface, since document selectors do not cross the
shadow boundary. A slider or canvas that owns a drag gesture can override
`touch-action` on that surface. Limit the override to the gesture's target;
avoid disabling zoom across the page or enclosing reading content.

---

## Native dialogs

Follow [Add a browser module](../extend/plugins.guide.md#add-a-browser-module)
for the complete repository entrypoint. Replace its `plugins/counter/client.ts`
with the code below and open `/counter.page`. The page receives an Open help
button; click it, dismiss with Escape, and confirm focus returns to that button.

Browser clients that render ordinary HTML can use `nativeModal` from
`@pathmx/core/controls/browser`. Initialize it once for each mounted dialog:

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

mountPathMXBehavior("[data-counter]", (root, { signal }) => {
  const trigger = document.createElement("button")
  trigger.textContent = "Open help"
  const dialog = document.createElement("dialog")
  dialog.className = "pmx-dialog"
  dialog.setAttribute("aria-label", "Help")
  const close = document.createElement("button")
  close.textContent = "Close"
  dialog.append("Choose a lesson to begin. ", close)
  root.append(trigger, dialog)

  const modal = nativeModal(dialog)
  trigger.addEventListener("click", () => modal.open(trigger), { signal })
  close.addEventListener("click", () => modal.close(), { signal })
  return { dispose() { modal.dispose(); trigger.remove() } }
})
```

Opening uses the native modal focus behavior and locks background scrolling.
Escape, a pointer press and release outside the dialog, or `close()` dismisses
it and restores scroll and focus. Opening another managed native dialog closes
the previous one. Dialog key events do not trigger document-level shortcuts.

Optional `onClose` and `returnFocus` callbacks let the owner react to dismissal
and select a replacement focus target when its trigger changes. `dispose()`
closes and removes the dialog; create a new lifecycle for a new element.
For React-owned interfaces, use the Dialog primitives in the next example.


---

## Add a React dialog

Use an initialized [Quickstart project](../start/first-site.guide.md). Install:

```sh
bun add @pathmx/react@beta @pathmx/ui@beta react@^19 react-dom@^19
bun add --dev @types/react @types/react-dom
```

`@pathmx/ui` is a library, so it needs no `pathmx.plugins` activation. Create
`plugins/help.plugin.ts`:

```ts
import { defineComponent, moduleRef } from "@pathmx/core"

export default defineComponent({
  tag: "x-help-dialog",
  render: () => ({
    html: '<div><details><summary>Help</summary><p>Choose a lesson to begin.</p></details></div>',
    clientData: {},
  }),
  client: moduleRef(() => import("./help.client.tsx")),
})
```

The native disclosure is useful before JavaScript loads or when it is unavailable.
The browser app replaces the region with a dialog trigger.

---

## Mount and try the dialog

Create `plugins/help.client.tsx`:

```tsx
import { reactClient } from "@pathmx/react"
import {
  Dialog, DialogTrigger, DialogContent,
  DialogTitle, DialogDescription, DialogClose,
} from "@pathmx/ui"

export default reactClient(() => (
  <Dialog>
    <DialogTrigger className="pmx-button">Open help</DialogTrigger>
    <DialogContent>
      <DialogTitle>Help</DialogTitle>
      <DialogDescription>Choose a lesson to begin.</DialogDescription>
      <DialogClose className="pmx-button">Close</DialogClose>
    </DialogContent>
  </Dialog>
))
```

Create `paths/help.page.md`:

```md
# Workshop help

<x-help-dialog />
```

Run `bun run dev` and open `/help.page`. Open help, move through its controls with
Tab, then close with Escape or Close. Focus returns to Open help. Try the same
interaction on a phone and in Player. `mountPathMXApps` owns mounting and cleanup
when that host leaves the page; see the [React lifecycle](../extend/react.reference.md#mount-a-source-backed-app).

Core's Theme Plugin supplies `.pmx-button` and `.pmx-dialog` styling. The UI package
loads React-specific styles; an explicit CSS entrypoint is available as
`@pathmx/ui/styles.css`.

---

## Split actions

Wrap a primary action and adjacent chooser button in `.pmx-split-control`.
Use `.pmx-button` on both direct children. They share outer corners and a divider
while retaining separate keyboard focus targets. Use an anchor for navigation;
never nest the chooser button inside that anchor. The owning plugin supplies
chooser behavior and hides its trigger until it is ready.

Player provides a complete split action:

```md
[@link.play Play this guide](../learning/player.reference.md)
```

The primary link starts Focus. The chevron offers Focus or Slides and starts the
chosen mode immediately. `.play.slides` makes Slides the default. See the
[Player reference](../learning/player.reference.md#link-to-a-reading-mode).


---

## Floating islands and dialogs

Use `.pmx-island` for a floating card that augments the surrounding content:

```html
<aside class="pmx-island source-details" aria-label="Selected Source">
  <a href="/lessons/color.lesson">Color relationships</a>
  <p>Lesson</p>
</aside>
```

The class supplies the surface and padding; your CSS owns placement and content
layout. Floating buttons (`data-variant="floating"`) and native/React
`.pmx-dialog` surfaces share its translucent background, border, blur, and
elevation. Buttons retain their circular or pill shape. Dialog behavior remains
with native HTML or the React adapter.

| Variable | Default |
| --- | --- |
| `--pmx-island-background` | Theme surface at 84% opacity |
| `--pmx-island-border` | `var(--pmx-color-border)` |
| `--pmx-island-radius` | `1.75rem` |
| `--pmx-island-shadow` | `var(--pmx-shadow-lg)` |
| `--pmx-island-blur` | `20px` |
| `--pmx-dialog-radius` | `var(--pmx-island-radius)` |
| `--pmx-dialog-shadow` | `var(--pmx-shadow-xl)` |
| `--pmx-dialog-backdrop` | Black at 44% opacity |

Override tokens globally or on the surface itself:

```css
.source-details {
  --pmx-island-radius: 2rem;
  --pmx-island-background: var(--pmx-color-surface);
  --pmx-island-blur: 0px;
}
```

Rules have low specificity in the foundation layer, so ordinary CSS can also
override individual properties. Put global tokens on `:root` when they must
reach React portals. Browsers without backdrop blur and readers requesting
reduced transparency receive a solid theme surface.

---

## Progress and task status

Opt into progress styling with native HTML. The meter fills the available
container width beside a compact count:

```html
<div class="pmx-progress">
  <progress value="2" max="3" aria-label="2 of 3 complete">2 of 3 complete</progress>
  <span class="pmx-progress-count" aria-hidden="true">2/3</span>
</div>
```

`.pmx-task-row` aligns a task's status or checkbox with its content.
`.pmx-task-status` styles a read-only indicator; give it `role="img"`, an
`aria-label`, and `data-state="complete"` or `"incomplete"`. Use native
checkboxes for editable tasks. The owning plugin supplies actions and permissions.

| Variable | Default | Purpose |
| --- | --- | --- |
| `--pmx-progress-meter-width` | `100%` | Meter width before making room for the count. Set a length such as `4rem` for a compact meter. |
| `--pmx-progress-meter-height` | `.375rem` | Meter thickness. |
| `--pmx-task-status-size` | `1rem` | Status and checkbox dimensions. |
| `--pmx-task-row-gap` | `.65rem` | Space between status and task content. |

Colors use the existing accent, muted, border, and focus tokens. These styles
apply only to opted-in elements. Completion uses them for progress and outlines;
Path reuses the count styling for linked activities.

---

## Icon catalog

`@pathmx/core/icons` provides portable SVG geometry and rendering. Use `icons`
to inspect supported names or `hasIcon(name)` to check a value before rendering:

```ts
import { icons, hasIcon, renderIcon } from "@pathmx/core/icons"

const names = Object.keys(icons)
const svg = renderIcon("clipboard-list", { size: 20 })
```

`renderIcon` returns a decorative SVG string; give its surrounding button or
link an accessible name. An unknown string name returns `undefined`. In React,
use `Icon` from `@pathmx/ui`. [Maps](../authoring/maps.reference.md) use the same
catalog for type icons.

The optional Settings UI is configured through
[Personal state and settings](../extend/state.reference.md#add-device-settings).
