npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

astro-magic-move

v0.4.0

Published

Animated code morphing for Astro, powered by Shiki Magic Move. Build-time tokenization, zero-framework client JS, CSS-variable theming.

Readme

astro-magic-move

Animated code morphing for Astro, powered by Shiki Magic Move.

  • Zero-config precompilation — tokenization runs in Astro's frontmatter at build time. No highlighter ships to the client.
  • Built-in triggers — scroll into view, click to advance, or auto-play on mount. No wiring required.
  • CSS-variable theming — token colors come from --shiki-* custom properties instead of inline styles. Works with Tailwind, daisyUI, plain CSS, whatever.
  • Tiny client footprint — only the MagicMoveRenderer (~4 kB) runs in the browser.

Demo & docs

How is this different from shiki-magic-move?

I loved the shiki-magic-move package, and wanted to make it easier to add into Astro projects.

shiki-magic-move provides the core diffing engine, renderer, and framework components (React, Vue, Svelte, Solid, web components) that power the animation. It also supports a precompiled path where you tokenize at build time and ship only the renderer to the client.

astro-magic-move builds on top of that. Instead of manually creating a highlighter, running createMagicMoveMachine, serializing tokens, and writing a client-side consumer, you pass code strings as props and the component handles everything:

<MagicMove before={a} after={b} trigger="scroll" />

On top of the DX simplification, it adds two features not present in shiki-magic-move:

  • Trigger modes (scroll, click, auto) — shiki-magic-move leaves it to you to decide when transitions fire. This component has them built in.
  • CSS-variable theming by default — shiki-magic-move applies token colors as inline styles. This component uses Shiki's css-variables theme so you can control all syntax colors with --shiki-* custom properties.

Install

pnpm add astro-magic-move

Import the base styles once (e.g. in a layout):

---
import 'astro-magic-move/styles'
---

Usage

Before / After

---
import { MagicMove } from 'astro-magic-move'

const before = `const data = fetch('/api')`
const after = `const data = await fetch('/api')
const json = await data.json()`
---

<MagicMove
  before={before}
  after={after}
  lang="typescript"
  trigger="click"
/>

Multi-step

---
import { MagicMove } from 'astro-magic-move'

const steps = [
  `const x = 1`,
  `const x = 1
console.log(x)`,
  `function log(val: number) {
  console.log(val)
}
log(1)`,
]
---

<MagicMove
  steps={steps}
  lang="typescript"
  trigger="click"
/>

Trigger modes

<!-- Auto-play on mount -->
<MagicMove before={a} after={b} trigger="auto" />

<!-- Click to advance steps -->
<MagicMove steps={steps} trigger="click" />

<!-- Scroll into viewport -->
<MagicMove before={a} after={b} trigger="scroll" />

<!-- No built-in trigger -->
<MagicMove steps={steps} trigger="none" />

External control

Use trigger="none" and drive steps from your own code:

<MagicMove id="demo" steps={steps} lang="typescript" trigger="none" />

<button id="prev">Prev</button>
<button id="next">Next</button>

<script>
  const el = document.querySelector('#demo')
  document.getElementById('next').addEventListener('click', () => {
    el.step = Math.min(el.step + 1, el.totalSteps - 1)
  })
  document.getElementById('prev').addEventListener('click', () => {
    el.step = Math.max(el.step - 1, 0)
  })
</script>

The step setter animates to the given index. Read el.step for the current position and el.totalSteps for the count. External control also works alongside built-in triggers.

Waiting for the element to be ready

The component's script is bundled as a deferred module, so the custom element isn't defined — and .totalSteps / .isReady aren't meaningful — the instant the HTML is parsed. Wait for the class to be registered, then either check isReady or listen for magic-move:ready:

customElements.whenDefined('magic-move').then(() => {
  const el = document.querySelector('magic-move')
  if (el.isReady) init()
  else el.addEventListener('magic-move:ready', init, { once: true })
})

function init() {
  // safe to read .totalSteps and write .step
}

whenDefined(...).then(...) works from both module and inline (is:inline) scripts. Calls to el.step = N before the element is ready are queued and applied on ready, so simple "jump to step on load" use cases work without waiting.

Loading code from real source files

Template literals in frontmatter drift from the real source — if you refactor the file, the animation silently lies. Use Vite's ?raw import to read the real file, then pair with the slice / region helpers:

---
import { MagicMove } from 'astro-magic-move';
import { slice, region } from 'astro-magic-move/helpers';
import gen from '../src/generate.ts?raw';
---

<!-- explicit line range, 1-indexed inclusive -->
<MagicMove steps={[
  slice(gen, 10, 17),
  slice(gen, 10, 23),
  slice(gen, 10, 30),
]} lang="ts" trigger="click" />

For refactor-safe ranges, mark regions in the source with VS Code's standard #region comments (named — plain unnamed // #region is ignored):

// generate.ts
// #region v1-schema
const spec = Schema.parse(output);
// #endregion

// #region v2-structural
const spec = Schema.parse(output);
assertUniqueIds(spec);
// #endregion
<MagicMove steps={[
  region(gen, 'v1-schema'),
  region(gen, 'v2-structural'),
]} lang="ts" trigger="click" />

region strips marker lines and de-indents by the minimum common indent, so a block extracted from inside a function renders flush-left. Both helpers throw at build time on missing names or out-of-range indices — a missing region fails the build with a clear error rather than shipping a blank animation.

?raw imports resolve the file via Vite, so project-relative paths, TS path aliases, and workspace imports all work. Files outside the Astro project root need server.fs.allow configured in astro.config.mjs.

Nested #region markers of any name are depth-counted (an inner #endregion won't close the outer region) and stripped from the output. VS Code also folds unnamed // #region; those are ignored by region() since extraction needs a name to target.

Theming

Define --shiki-* CSS custom properties to control syntax colors:

:root {
  --shiki-foreground: #d4d4d4;
  --shiki-background: #1e1e1e;
  --shiki-token-keyword: #c586c0;
  --shiki-token-string: #ce9178;
  --shiki-token-function: #dcdcaa;
  --shiki-token-comment: #6a9955;
}

Sensible defaults are built in — the component works without defining any variables.

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | before | string | — | Code for initial state (two-step shorthand) | | after | string | — | Code for final state (two-step shorthand) | | steps | string[] | — | Array of code strings (min 2) | | lang | string | 'typescript' | Shiki language grammar | | trigger | 'scroll' \| 'click' \| 'auto' \| 'none' | 'scroll' | How the animation fires | | duration | number | 800 | Animation duration in ms | | stagger | number | 0.3 | Token entrance stagger | | threshold | number | 0.4 | IntersectionObserver threshold (scroll only) | | lineNumbers | boolean | false | Show line numbers | | class | string | — | CSS classes on outer element |

DOM API

The <magic-move> custom element exposes these properties for external control:

| Property | Type | Description | |----------|------|-------------| | .step | number (get/set) | Current step index. Setting it animates to that step. Writes before isReady are queued and applied on ready. | | .totalSteps | number (get) | Total number of steps. Returns 0 until the element is ready. | | .isReady | boolean (get) | true once the element has hydrated. |

The element dispatches two events:

  • magic-move:ready — fires once, after hydration, when .step / .totalSteps are safe to use.
  • magic-move:step — fires after each transition with { step, total } detail.
document.querySelector('magic-move')?.addEventListener('magic-move:step', (e) => {
  console.log(`Step ${e.detail.step + 1} of ${e.detail.total}`)
})

TypeScript

Importing from astro-magic-move augments HTMLElementTagNameMap and HTMLElementEventMap, so the DOM API is typed with no casts or generic parameters:

import 'astro-magic-move';

const el = document.querySelector('magic-move');
if (el) {
  el.step = 2;                         // typed number setter
  const n: number = el.totalSteps;     // typed
  el.addEventListener('magic-move:step', (e) => {
    e.detail.step;                      // typed as number
    e.detail.total;                     // typed as number
  });
}

For consumers who want to reference the types explicitly: MagicMoveElement, MagicMoveStepEvent, and MagicMoveReadyEvent are exported from the package root.

License

MIT