Player

Player turns a Source into focused reading or slides. Blocks and Beats choreograph learning: Blocks frame a topic or activity; Beats guide attention through it.

Try Player FocusSlides

Add Player

Start with an initialized Quickstart project. Activate Player in its root:

bun run pmx plugins add player

Place its app component in a page, or in a shared layout to offer Player across the site. Use one app per page.

<x-player-app />

The app shows a Play button while reading. Play starts in Slides by default; an explicitly chosen mode is remembered by the session or installed Settings. Start Play to reveal Player controls for keyboard shortcuts and settings, or follow a Player link. Exiting Play hides Player controls again.

Player keeps control preferences for the current session. Install the shared Settings plugin to save them across reloads; Settings owns their browser storage.

With Paths, playback keeps the selected Source instance and Path placement. A compact Path control joins Player's chrome; its outline and step navigation are separate from the current Source's Block/Beat navigation.

Try a two-Block lesson

Create paths/contrast.lesson.md:

# Notice contrast

<x-player-app />

Dark text on a light background is easier to distinguish than similar grays.

---

<!-- id: practice -->

## Try it

Find two objects near you with different brightness. Which is easier to notice?

Run bun run dev and open /contrast.lesson. If your shared layout already contains <x-player-app />, omit it from this lesson so there is one app per page. Choose Play: Focus keeps the reading flow and guides attention through the title, explanation, and activity. Switch to Slides: the explanation and practice occupy two successive Blocks. Advance and go back, then exit to return to reading.

Add these links to paths/index.md:

[@link.play.focus Read with focus](./contrast.lesson.md)
[@link.play.slides Present the lesson](./contrast.lesson.md)

Both open the same authored lesson. Add #practice to either target to begin at the activity. If the launch control is missing, check package activation and app placement; no separate slide file is needed.

[@link.play Play the lesson](../../examples/color-context.demo.md)

Play the lesson FocusSlides

DirectiveDefault mode
@link.playSlides
@link.play.focusFocus
@link.play.slidesSlides

Each renders one split control: the primary link starts in the authored default mode, and the chevron opens a Focus/Slides chooser. Choosing a mode starts it immediately. The qualifier sets the default; both modes remain available. The authored label and default stay stable, independent of previously used modes. The destination needs the Player app; the linking page does not. Primary and chooser links support copying and opening in another tab. The chooser supports arrow keys, Home/End, Escape, and outside dismissal. Player controls do not open Source previews. Without JavaScript, the primary link still works and the chooser stays hidden.

Fragments and other query parameters are preserved. The equivalent URL parameters are ?play=focus and ?play=presentation.

An explicit play parameter selects that mode. Ordinary page links carry the current Player settings. The chooser marks this page’s active mode while playing, or the authored default while reading. Following the current mode preserves your position; switching modes on this page also preserves your active Beat unless the link names a different fragment. Links to another lesson, a different Beat, or different settings remain navigation actions.

A link to an authored Block id, such as #practice, starts at that Block's first Beat. If live content removes the active Block, Player returns to the nearest preceding Block still present in the document.

The active Beat appears in the URL, so copied links and browser history return to that selection.

Configure playback from a URL

Use these parameters with play=focus or play=presentation, including inside <x-source-embed />. URL values override the current preferences. Omitted settings use those preferences; configuration controls are shown by default.

ParameterValuesEffect
playfocus, presentationStart Player in the chosen mode.
intensitylow, medium, highSet the emphasis on surrounding Beats.
themedefault or an installed theme IDChoose the plain foundation or a presentation theme.
play-guideon, offShow or hide the Beat Guide.
play-noteson, offOffer or hide presenter notes.
play-autohideon, offFade idle controls when the Beat has no actions. Beat actions and touch controls remain visible.
play-controlson, offShow or remove the Settings button and configuration panel.

For a slide embed with visible authored actions and no configuration UI:

<x-source-embed src="./lesson.md?play=presentation&intensity=low&play-controls=off&play-notes=off&play-autohide=off" />

play-controls=off leaves authored actions, navigation, progress, and the touch exit button available. Use play-guide=off and play-notes=off separately when those should also be hidden. This flag only controls the presentation of the UI; keyboard shortcuts still work. It is not saved as a personal preference.

Current settings travel with ordinary same-origin Source links and copied URLs. Guide, notes, and auto-hide serialize explicitly as on or off so reloading preserves the selected value. Stopping Play removes Player parameters from the URL.

Choreograph the learning sequence

A Block holds a coherent learning moment: an explanation, a worked example, or an activity. A Beat is one point of attention within that moment: a paragraph to consider, a diagram to inspect, or a move on a chessboard.

For example, a Block might explain a chess tactic. Its Beats guide the learner through the question, the position, and the move that reveals the answer. The next Block can ask the learner to try the idea.

Start with normal Markdown. Player derives Beats from headings, paragraphs, list items, media, code, tables, callouts, and other meaningful content. You do not need to label every paragraph. Review the sequence by playing it: each advance should help the reader notice or do something useful.

Use Block boundaries when the topic or activity changes. In Slides, each Block becomes a screen; in Focus, the page keeps its reading flow. Beats provide the smaller steps within that structure. Readers can move at their own pace, revisit a Beat, or return to normal reading.

Author an explicit Beat

Use an explicit Beat for a meaningful step in an authored component:

<p data-pmx-beat="observation">Describe one thing you noticed.</p>

A callout is one Beat: its title, icon, and body share the same intensity. Authored components are also grouped by default. For custom HTML, put data-pmx-beat on the meaningful outer element to keep its labels and content in one step:

<aside data-pmx-beat="practical-tip">
  <strong>A practical tip</strong>
  <p>Draw the tree once, then trust the base case.</p>
</aside>

Player applies intensity to that outer element, so its contents fade together. No Player-specific CSS classes are needed. Use explicit child Beats when the component deliberately reveals several separate steps.

Hidden Blocks contribute no Beats. Revealing an earlier Block preserves the identities of Beats in later Blocks, so the active content stays anchored. Presentational HTML wrappers (role="presentation" or role="none") let their children supply Beats instead of becoming Beats themselves.

The stable name also becomes a fragment target. A link ending in ?play=focus#observation opens that Beat. Use distinct names within the page.

The bookcase uses books as Beats; the chess components use game positions. Their literate sources keep the behavior alongside its explanation and examples.

Player supplies aria-current="step" for the active Beat when it has no authored aria-current. Components that manage their own current position should set aria-current="step" on that position and aria-current="false" on the others. Player preserves those values during navigation and when playback stops.

Open Player controls → Keyboard shortcuts for the available bindings. Shortcuts are grouped into navigation, reading mode, and Beat actions.

Back and Next in the control row step through the sequence: Next shows the next Beat, then the next slide, like ↓ or Space. ← / →, horizontal swipes, and the Block grid jump between Blocks.

Focus follows the nearest Beat after you scroll. Slides uses Player navigation to move through the content. The Block grid gives an overview of the sequence. Focus follows document scrolling even when it arrives without a touch or wheel event, such as scrolling through accessibility controls. Player navigation keeps the selected Beat while it moves the page.

Slides show one Block at a time. Content that fits is centered in a full-height frame; longer content keeps its readable size and scrolls with the page, so touch scrolling, focused fields and the on-screen keyboard behave as on any page. Moving between Blocks shows the next slide from its top. Moving between Beats within a tall slide scrolls the page just enough to show the Beat. Returning to a Block's opening Beat returns to its top. Scrolling past a tall slide's first screen reveals the Beats you scroll to; Next continues from the current Beat. Within a slide, Beats already shown stay at full strength, and intensity sets how strongly upcoming Beats are dimmed. Frames reserve space below the content for the compact controls, and fixed controls stay above an on-screen keyboard.

On phones, Slides show each slide whole, and Next moves slide by slide. Beats you author with data-pmx-beat, and code steps, remain steps within a slide; derived Beats such as paragraphs and list items do not. A link to a finer Beat opens on the slide that presents it. Swipe horizontally to change slides. Taps on content never navigate, and swipes that start on an answer, its label, or any control stay with that control.

In Focus, tap a paragraph, list item, or table row to move the guide there without scrolling the page. Links and editable controls keep their normal behavior. On touch screens, Medium intensity keeps surrounding text readable; Low leaves it fully visible, and High provides stronger emphasis.

While playing, the floating Player controls button opens navigation and reading settings, including when the site also provides shared Settings. On phones, these controls open in a bottom drawer.

Keyboard shortcuts

KeyAction
⌘ Enter / Ctrl EnterStart Player; while playing, submit the focused response or the sole available submit action
↓, Page Down, SpaceNext Beat
↑, Page Up, Shift SpacePrevious Beat
→ / ←Next / previous Block
GOpen or close the Block grid
MSwitch between Focus and Slides
+ / −Increase / decrease intensity
1–9Activate a numbered Beat action
EscapeClose the grid or exit Player

In the grid, arrow keys select a Block and Enter or Space opens it. Inputs retain their editing keys, and focused controls retain native activation. Settings and open dialogs handle their own keyboard interactions.

Respond in Player

Input responses offer their current Save action while you work anywhere in that response, including responses spanning several Blocks. Save appears when the current response is ready. Required fields, custom editor issues, permissions, and pending submission all contribute to that decision. Assessment keeps its own attempt and retry rules.

Responses with editable text fields offer one entry action throughout their Blocks. A single field uses its authored field label. Several fields use the response's input-response.label when supplied, otherwise the target field's label. Entry reaches the first incomplete or invalid text editor, falling back to the first editable text field when all are valid. It stays hidden while an editor in that response has focus.

Player invokes the same operation as the original submit control. It shows Saving… while that operation runs, then briefly Saved. Editing during submission keeps the newer draft. If an action disappears after keyboard activation, focus returns to the active Beat unless you have moved elsewhere.

  • Press Enter from the reading surface to use its one available response entry.
  • Press ⌘ Enter or Ctrl Enter inside an editor to submit its one available response. Outside an editor, the same shortcut submits only when the action row has one unambiguous submit action. Submit actions show this shortcut instead of a number. Composing text and repeated key events do not submit.
  • Press Escape inside an editor to return focus to the active Beat, then use ← / → to move between Blocks.
  • Use Back and Next to leave an editor directly. The control bar stays visible while editing. Moving from an editor to another Block focuses its heading without opening another editor, and unsaved drafts remain when you return.

Ordinary Enter and arrow keys inside editors retain their editing behavior. Touch navigation does not automatically open the keyboard.

Guide a form

A questionnaire can open directly in Player as a guided form: one question per slide, with only segmented progress and floating Back and Next controls. Set it on the form's Source:

---
play:
  start: presentation
  chrome: participant
---

# Congregational survey

Welcome. The survey takes about fifteen minutes.

<x-player-app />

---

<!--
id: worship
play:
  variant: section
-->

## Worship

---

<!-- id: attendance -->

<fieldset>
<legend>How often have you attended services this year?</legend>
<x-input-radio name="answer" value="weekly" label="Weekly" required />
<x-input-radio name="answer" value="monthly" label="Monthly" required />
</fieldset>

---

<!--
id: save-worship
play:
  skip: true
-->

<x-input-submit blocks="attendance" label="Save section" />

play.start opens Player on load in that mode, like a ?play= link. With chrome: participant:

  • Each Block is one slide. The action reads Start on the first slide, Skip while an optional question has no answer, and Next otherwise. It, or Enter from a choice or single-line field, checks the slide's responses before moving on. An unanswered required question keeps the participant on the slide and says why.
  • In a written answer, Enter adds a line and ⌘ Enter or Ctrl Enter moves on. With a keyboard or mouse, arriving at a written question places the cursor in it; on touch screens the keyboard stays closed until the field is tapped.
  • Leaving the last slide of a submit group saves that group first. The move happens only after the save succeeds; a failure stays on the slide with its reason.
  • Questions revealed by an answer are included: Next waits for the latest successful draft preview. Failure or timeout keeps the current slide open, and Next retries. Validation uses Input’s authored field labels and focuses the failing editor.
  • Back never checks or saves. Drafts remain until they are saved.
  • An answered optional choice question offers Clear answer, which removes the selection (including every row of a rating grid and any “Other” text) so the question saves with no answer. Required and written questions do not show it; written answers are edited in place.
  • Returning participants start at the first unsaved response. Content can name its own starting slide instead: an element with data-pmx-play-start, such as a submission receipt, opens Player on the slide that contains it.
  • Progress shows one segment per section slide, filling as questions are passed, with the current section's name above the question. A section slide itself is centered and marked with its place, such as “Section 2 of 5”.
  • Settings, mode switching, intensity, notes, the grid, exit, swipes and tap zones are unavailable. The rest of the page reads normally without JavaScript.

play.skip: true keeps a Block out of Player in every mode while it still reads in the page, which suits a long-form save button. Blocks with no text or media are never slides. Section headings with play.variant: section become short interstitials.

Style the presentation

Player uses the shared site themes. Choose a Source theme with normal frontmatter:

---
theme: paper
---

A Block can choose a presentation variant in its topmatter:

---

<!-- play: { variant: cover } -->

## A question worth asking

What makes this example useful?

Available variants are cover, section, quote, fact, full, and end. Use variants for emphasis while keeping the Block understandable in normal reading.

Beat actions

Mark a component's native control with data-pmx-play-affordance to make it available in the active Beat's action strip:

<button type="button" data-pmx-play-affordance>Run example</button>
<label><input type="checkbox" data-pmx-play-affordance /> Show comparison</label>
<label for="reflection">Reflection</label>
<textarea id="reflection" data-pmx-play-affordance></textarea>

Buttons activate the original control, including form validation and submission. Checkboxes toggle the original input and reflect its checked or mixed state; native change events keep the action in sync. Other fields offer a shortcut that focuses the original field. Links keep their normal destination and modifier-click behavior. Code, Input, and Video components mark their controls automatically.

Add data-pmx-play-icon to show an icon beside the label in the Beat action pill:

<button
  type="button"
  data-pmx-play-affordance
  data-pmx-play-icon="play"
  aria-label="Run experiment"
>
  Run experiment
</button>

Icon names come from @pathmx/core/icons. Player supplies the shared button appearance, a visible label, an accessible name, and a tooltip containing the label and numbered shortcut. Without a recognized icon, the action uses its text label.

The original control owns behavior and state. A plugin updates its aria-label, data-pmx-play-icon, and native disabled or ARIA state as the interaction changes. Player reflects those changes and activates that same control; it does not maintain a second copy of plugin state. Visible Web Component hosts with role="button" can contribute through the same attributes. Internal shadow-root controls remain owned by their component.

Give controls native labels and meaningful disabled, pressed, or expanded states. Player uses visible controls in the active Beat, excluding nested Beats and hidden or inert controls and generated heading permalinks. The original controls remain usable in every mode.

Use 1–9 to activate a numbered affordance, including while a button or link has focus. Numbers and other shortcut hints appear with a mouse or trackpad; on touch screens they appear once a hardware keyboard is used. Text inputs, sliders, and other value controls retain their native keys. Enter alone does not run the first action; Enter/Space on a focused control retain native behavior. Disabled or busy controls cannot be activated. Opaque shadow-root controls are not discovered.

Studio's authoring controls are hidden during Focus and Slides and do not become Beats. Exit Player to edit a Block.

Beat actions appear as small pills in one bottom row beside Notes and Settings. All controls share a 44px height and the same floating surface. Extra actions scroll horizontally instead of stacking over the content; long labels truncate visually while retaining their full accessible names. While the active Beat has actions, the row stays visible through pointer movement, idle time, and scrolling. With no actions, auto-hide can fade the row until pointer activity or keyboard focus returns. Touch controls remain visible.

Presenter notes

Enabled presenter notes open on demand in a bounded popover or touch sheet. Only one notes/settings surface opens at a time; Escape returns focus to its trigger. Numbered action shortcuts remain available. Short and narrow Slides reserve room below the content for the control row.

Lifecycle events

Player dispatches ordinary bubbling DOM events on the corresponding Block or Beat element. Plugins can respond without owning Player state or delaying navigation. At a Block boundary, order is player:beat-leave, player:block-leave, player:block-enter, then player:beat-enter. Within one Block, only the Beat pair fires. First entry emits the two enter events; exit emits the two leave events. Selecting the current Beat again emits nothing.

on(el, "player:beat-enter", (event) => {
  const { id, index, direction, cause, mode } = event.detail
  // Update this component for the entered Beat.
})

detail contains the Beat id, zero-based index, owning Block block ID, Source source ID, direction (-1, 0, or 1), cause, mode, intensity, and optional member information for table rows or code-line groups. Leave events describe the previous Beat; enter events describe the destination. Direction is zero on first entry, exit, or a change of Source.

Causes distinguish start, navigation, scroll, history, restore, update, and exit. Mounting or refreshing the same active Beat does not emit a new entry. If an update changes the active identity, events use update, allowing consumers to reconcile without treating the update as a user advance.

SFX binds sounds to these events. Other components can use the same events for their own behavior. Native bubbling means a listener on a containing element should check event.target when it needs only its own entry rather than entries from descendants.