Animate Code Using TypeScript

Published Mar 21, 2026

Table of Contents

Shiki Magic Move

Shiki Magic Move is a low-level library for animating code blocks using Shiki for syntax highlighting.

In a previous post I wrote about how to create animated code blocks using Shiki Magic Move in Svelte by creating a custom Svelte renderer. Shiki Magic Move provides framework wrappers so you donโ€™t have to make your own renderer, but it doesnโ€™t have instructions on how to use it with regular TypeScript.

In this post Iโ€™m going to show you how you can use TypeScript to create smoothly animated code blocks using Shiki Magic Move.

Animating Code Blocks Using TypeScript

If you want to follow along create a vanilla TypeScript project using Vite:

terminal
npm create vite@latest magic-move -- --template vanilla-ts

We need to install the shiki-magic-move and shiki package:

terminal
npm i shiki-magic-move shiki

At the heart of Shiki Magic Move is a framework agnostic core, and renderer:

magic-move.ts
import { codeToKeyedTokens, createMagicMoveMachine } from 'shiki-magic-move/core'
import { MagicMoveRenderer } from 'shiki-magic-move/renderer'

We also need to import the CSS styles for the animations from shiki-magic-move:

magic-move.ts
import 'shiki-magic-move/dist/style.css'

Next letโ€™s create the Shiki highlighter:

magic-move.ts
import { createHighlighter } from 'shiki'

// create the Shiki highlighter
const highlighter = await createHighlighter({
	langs: ['typescript'],
	themes: ['poimandres'],
});

Keep in mind how Shiki can be resource intensive. If youโ€™re going to create multiple highlighters you want to cache them using the Map object. You can also create lighter bundles by reading the Shiki docs.

Now itโ€™s time to create a MagicMove class that creates the machine and renderer with an update method:

magic-move.ts
import { createHighlighter, type HighlighterCore, type BundledLanguage, type BundledTheme } from 'shiki'
import { codeToKeyedTokens, createMagicMoveMachine, type MagicMoveDifferOptions, type MagicMoveRenderOptions } from 'shiki-magic-move/core'
import { MagicMoveRenderer } from 'shiki-magic-move/renderer'
import 'shiki-magic-move/dist/style.css'

interface MagicMoveOptions extends MagicMoveRenderOptions, MagicMoveDifferOptions {
	lang: BundledLanguage
	theme: BundledTheme
}

class MagicMove {
	// the machine diffs the code
	private machine: ReturnType<typeof createMagicMoveMachine>
	// the renderer creates DOM elements and transitions
	private renderer: MagicMoveRenderer

	constructor(target: Element, highlighter: HighlighterCore, code: string, options: MagicMoveOptions) {
		const { lang, theme, lineNumbers = false } = options

		// add `<pre>` element to target
		const pre = document.crateElement('pre')
		pre.className = 'shiki-magic-move-container'
		target.appendChild(pre)

		// create the Shiki Magic Move machine
		this.machine = createMagicMoveMachine(
			(code) => codeToKeyedTokens(highlighter, code, { lang, theme }, lineNumbers),
			options,
		);

		// create the Shiki Magic Move renderer
		this.renderer = new MagicMoveRenderer(pre, options)

		// initial render
		this.machine.commit(code)
		this.renderer.render(this.machine.current)
	}

	// update the DOM and perform the transitions
	update(code: string) {
		this.machine.commit(code)
		this.renderer.render(this.machine.current)
	}
}

// get the target element
const target = document.querySelector<HTMLDivElement>('#app')!

// the formatting is important because of `<pre>`
const steps = {
	before: `
function lerp(a, b, t) {
  return a + (b - a) * t
}`.trim(),
	after: `
function lerp(a, b, t) {
  const delta = (b - a) * t
  return a + delta
}`.trim(),
};

// create the `MagicMove` instance
const code = new MagicMove(target, highlighter, steps.before, {
	lang: 'typescript',
	theme: 'poimandres',
})

let toggle = false

document.addEventListener('click', () => {
	// update the code
	code.update(toggle ? steps.before : steps.after)
	toggle = !toggle
})

You can pass more options like lineNumbers, duration, stagger, containerStyle to remove the code background, and easing to add more character to the animation among other options:

shiki-magic-move.ts
const code = new MagicMove(target, highlighter, steps.before, {
	lang: 'typescript',
	theme: 'poimandres',
	lineNumbers: true,
	duration: 1_000,
	stagger: 0.3,
	containerStyle: false,
	easing: `linear(
	   0, 0.009, 0.035 2.1%, 0.141, 0.281 6.7%, 0.723 12.9%, 0.938 16.7%, 1.017,
	   1.077, 1.121, 1.149 24.3%, 1.159, 1.163, 1.161, 1.154 29.9%, 1.129 32.8%,
	   1.051 39.6%, 1.017 43.1%, 0.991, 0.977 51%, 0.974 53.8%, 0.975 57.1%,
	   0.997 69.8%, 1.003 76.9%, 1.004 83.8%, 1
	 )`,
});

I got the easing from the linear easing generator which can create linear() easings from JavaScript and SVGs.

Thatโ€™s it! Now you have a framework agnostic Shiki Magic Move instance that you can use anywhere your heart desires.

Support

You can subscribe on YouTube, or consider becoming a patron if you want to support my work.

Patreon
Found a mistake?

Every post is a Markdown file so contributing is simple as following the link below and pressing the pencil icon inside GitHub to edit it.

Edit on GitHub