comark-magic-move
Magic move
A code group whose tabs are steps, the tokens two steps share sliding to their new places.
export async function getUser(id: string) {
const res = await fetch(`/api/users/${id}`)
return res.json()
}export async function getUser(id: string) {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error(`User ${id}: ${res.status}`)
return res.json()
}const cache = new Map<string, Promise<User>>()
export function getUser(id: string) {
if (!cache.has(id)) cache.set(id, load(id))
return cache.get(id)!
}
async function load(id: string) {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error(`User ${id}: ${res.status}`)
return res.json() as Promise<User>
}::magic-move{title="Cache getUser"}
```ts [Fetch]
export async function getUser(id: string) {
const res = await fetch(`/api/users/${id}`)
return res.json()
}
```
```ts [Handle errors]
export async function getUser(id: string) {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error(`User ${id}: ${res.status}`)
return res.json()
}
```
```ts [Cache]
const cache = new Map<string, Promise<User>>()
export function getUser(id: string) {
if (!cache.has(id)) cache.set(id, load(id))
return cache.get(id)!
}
async function load(id: string) {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error(`User ${id}: ${res.status}`)
return res.json() as Promise<User>
}
```
::export default defineConfig({
plugins: [vue()],
})import magicMove from "comark-magic-move"
export default defineConfig({
plugins: [vue(), magicMove()],
})::magic-move{title="Add a plugin"}
```ts [Before]
export default defineConfig({
plugins: [vue()],
})
```
```ts [After]
import magicMove from "comark-magic-move"
export default defineConfig({
plugins: [vue(), magicMove()],
})
```
::Features
Section titled "Features"- Every code block inside
::magic-moveis one step, with a tab named by the label after its language. - The words and marks two steps share move to their new places, and the rest fade in or out.
- It reads the rendered code, so it works with Shiki or any other highlighter, or none.
- Without the script, the steps read one after another, each under its label.
- With reduced motion, the step changes without moving anything.
Write two or more code blocks inside ::magic-move:
const total = price * 2const total = price * rate::magic-move
```ts
const total = price * 2
```
```ts
const total = price * rate
```
::Labels
Section titled "Labels"Use the [label] after a fence's language to name its step's tab. A step without one is named by its position, such as Step 2.
const total = price * 2const total = price * rate::magic-move
```ts [Start]
const total = price * 2
```
```ts [Use the rate]
const total = price * rate
```
::Use the title prop to name the tabs for screen readers. The page doesn't show it.
::magic-move{title="Extract the rate"}Examples
Section titled "Examples"Keep what stays the same
Section titled "Keep what stays the same"Code two steps share moves, and code that changes fades. Change one thing per step so the reader can follow each move.
greet(name)greet(name, { formal: true })::magic-move
```ts [One argument]
greet(name)
```
```ts [Two arguments]
greet(name, { formal: true })
```
::Review a change instead
Section titled "Review a change instead"An animation shows one step at a time, so a reviewer can't see all the removed and added lines at once. For a change someone approves, write a ::diff.
Accessibility
Section titled "Accessibility"The steps follow the WAI-ARIA tabs pattern. The tabs are a tab list, named by the block's title, and each step is a tab panel named by its tab. Only the open tab is a tab stop, so Tab moves from it to the open step and then on.
When the reader prefers reduced motion, a tab opens its step without moving anything.
Keyboard interactions
Section titled "Keyboard interactions"| Key | Description |
|---|---|
ArrowLeft, ArrowRight |
On a tab, opens the previous or next step, wrapping at either end. On a right-to-left page they follow the reading direction. |
Home, End |
On a tab, opens the first or last step. |
Tab |
Moves focus from the open tab to its step. |
Magic move
Section titled "Magic move"titleThe name screen readers read for the tabs.
Code block
Section titled "Code block"[label]Default: Step 1, Step 2, …. The name of the step's tab.
Options
Section titled "Options"comark-magic-move takes no options. magicMove: false in comarkUi() turns it off.
Issues
Section titled "Issues"The plugin adds each problem to meta.issues, as Validation describes:
magic-move 1 has one code block, so there's nothing to move between; add a code block for each step, or use a plain code block.It reports:
- A block with fewer than two code blocks.
- Content that isn't a code block. The block drops it.
- A prop it doesn't know.
Each one has its own rule in Doctor.
Package
Section titled "Package"comark-ui installs and registers comark-magic-move. To use it alone, install it and pass the renderer's components:
pnpm add comark @comark/html comark-magic-moveimport { createHtmlRenderer } from "@comark/html";
import magicMove, { components } from "comark-magic-move/html";
const render = createHtmlRenderer({ plugins: [magicMove()], components });For Vue, React, or Svelte, import from comark-magic-move/vue, comark-magic-move/react, or comark-magic-move/svelte. On a server, import comark-magic-move/style.css with your styles. Renderers covers the rest.
The tabs follow the shared tokens in Theming, and two variables set the header and the space between steps. The code keeps its highlighter's colors. The rules sit in a comark CSS layer, so any plain rule of yours wins.
Shared tokens
Section titled "Shared tokens"| Token | Used for |
|---|---|
--comark-background |
The open tab |
--comark-foreground |
The open and hovered tabs' text |
--comark-muted |
The other tabs' text, and the captions without the script |
--comark-border |
The header's border |
--comark-accent |
The header's surface |
--comark-ring |
The focus ring |
--comark-radius |
The corners |
Variables
Section titled "Variables"Set these on .cmv or any ancestor, such as :root:
| Variable | Default | Used for |
|---|---|---|
--cmv-surface |
--comark-accent at 50% |
The header behind the tabs |
--cmv-gap |
1rem |
The space between steps when they read in turn, without the script |
Classes and attributes
Section titled "Classes and attributes"| Selector | Is |
|---|---|
.cmv |
The block, with comark-code's data-code-header, so each step's buttons sit in the tabs' strip |
.cmv-list |
The tabs, shown once the script runs |
.cmv-tab |
A tab, with aria-selected="true" when its step is open |
.cmv-stage |
The steps |
.cmv-step |
Each step, with data-inactive on every step but the open one |
.cmv-caption |
A step's label above its code, shown without the script |
.cmv-t |
A word or mark the script cut from the code, on the first move |
[data-magic-move-ready] |
On <html> once the script runs. Without it, every step shows |
Example
Section titled "Example"A header that matches the code block:
:root {
--cmv-surface: var(--comark-background);
}To try it, paste it into Extra CSS in the playground's Theme pane.