Smoothly animated code blocks with Shiki. Online Demo.
Shiki Magic Move is a low-level library for animating code blocks, and uses Shiki as the syntax highlighter. You usually want to use it with a high-level integration like Slidev.
At the core of the shiki-magic-move
package is a framework-agnostic core, and renderer — there are also framework wrappers for Vue, React, and Svelte.
Each of the framework wrappers provides the following components:
ShikiMagicMove
- the main component to wrap the code blockShikiMagicMovePrecompiled
- animations for compiled tokens, without the dependency on ShikiShikiMagicMoveRenderer
- the low-level renderer component
The ShikiMagicMove
component requires you to provide a Shiki highlighter instance, and the styles are also required, and provided by shiki-magic-move
. Whenever the code
changes, the component will animate the changes.
You're going to need Shiki Magic Move for animating the code blocks, and Shiki for syntax highlighting.
npm i shiki-magic-move shiki
Import shiki-magic-move/vue
, and pass the highlighter instance to the ShikiMagicMove
component.
<script setup>
import { getHighlighter } from 'shiki'
import { ShikiMagicMove } from 'shiki-magic-move/vue'
import { ref } from 'vue'
import 'shiki-magic-move/dist/style.css'
const highlighter = await getHighlighter({
themes: ['nord'],
langs: ['javascript', 'typescript'],
})
const code = ref(`const hello = 'world'`)
function animate() {
code.value = `let hi = 'hello'`
}
</script>
<template>
<ShikiMagicMove
lang="ts"
theme="nord"
:highlighter="highlighter"
:code="code"
:options="{ duration: 800, stagger: 0.3, lineNumbers: true }"
/>
<button @click="animate">
Animate
</button>
</template>
Import shiki-magic-move/react
, and pass the highlighter instance to the ShikiMagicMove
component.
import { useEffect, useState } from 'react'
import { getHighlighter, type HighlighterCore } from 'shiki'
import { ShikiMagicMove } from 'shiki-magic-move/react'
import 'shiki-magic-move/dist/style.css'
function App() {
const [code, setCode] = useState(`const hello = 'world'`)
const [highlighter, setHighlighter] = useState<HighlighterCore>()
useEffect(() => {
async function initializeHighlighter() {
const highlighter = await getHighlighter({
themes: ['nord'],
langs: ['javascript', 'typescript'],
})
setHighlighter(highlighter)
}
initializeHighlighter()
}, [])
function animate() {
setCode(`let hi = 'hello'`)
}
return (
<div>
{highlighter && (
<>
<ShikiMagicMove
lang="ts"
theme="nord"
highlighter={highlighter}
code={code}
options={{ duration: 800, stagger: 0.3, lineNumbers: true }}
/>
<button onClick={animate}>Animate</button>
</>
)}
</div>
)
}
Import shiki-magic-move/solid
, and pass the highlighter instance to the ShikiMagicMove
component.
import { getHighlighter, type HighlighterCore } from 'shiki'
import { ShikiMagicMove } from 'shiki-magic-move/solid'
import { createResource, createSignal } from 'solid-js'
import 'shiki-magic-move/dist/style.css'
function App() {
const [code, setCode] = createSignal(`const hello = 'world'`)
const [highlighter] = createResource(async () => {
const newHighlighter = await createHighlighter({
themes: Object.keys(bundledThemes),
langs: Object.keys(bundledLanguages),
})
return newHighlighter
})
function animate() {
setCode(`let hi = 'hello'`)
}
return (
<div>
<Show when={highlighter()}>
{highlighter => (
<>
<ShikiMagicMove
lang="ts"
theme="nord"
highlighter={highlighter()}
code={code()}
options={{ duration: 800, stagger: 0.3, lineNumbers: true }}
/>
<button onClick={animate}>Animate</button>
</>
)}
</Show>
</div>
)
}
Import shiki-magic-move/svelte
, and pass the highlighter instance to the ShikiMagicMove
component.
<script lang='ts'>
import { getHighlighter } from 'shiki'
import { ShikiMagicMove } from 'shiki-magic-move/svelte'
import 'shiki-magic-move/dist/style.css'
const highlighter = getHighlighter({
themes: ['nord'],
langs: ['javascript', 'typescript'],
})
let code = $state(`const hello = 'world'`)
function animate() {
code = `let hi = 'hello'`
}
</script>
{#await highlighter then highlighter}
<ShikiMagicMove
lang='ts'
theme='nord'
{highlighter}
{code}
options={{ duration: 800, stagger: 0.3, lineNumbers: true }}
/>
<button onclick={animate}>Animate</button>
{/await}
ShikiMagicMovePrecompiled
is a lighter version of ShikiMagicMove
that doesn't require Shiki. It's useful when you want to animate the compiled tokens directly. For example, in Vue:
<script setup>
import { ShikiMagicMovePrecompiled } from 'shiki-magic-move/vue'
import { ref } from 'vue'
const step = ref(1)
const compiledSteps = [/* Compiled token steps */]
</script>
<template>
<ShikiMagicMovePrecompiled
:steps="compiledSteps"
:step="step"
/>
<button @click="step++">
Next
</button>
</template>
To get the compiled tokens, you can run this somewhere else and serialize them into the component:
import { getHighlighter } from 'shiki'
import { codeToKeyedTokens, createMagicMoveMachine } from 'shiki-magic-move/core'
const shiki = await getHighlighter({
theme: 'nord',
langs: ['javascript', 'typescript'],
})
const codeSteps = [
`const hello = 'world'`,
`let hi = 'hello'`,
]
const machine = createMagicMoveMachine(
code => codeToKeyedTokens(shiki, code, {
lang: 'ts',
theme: 'nord',
}),
{
// options
}
)
const compiledSteps = codeSteps.map(code => machine.commit(code).current)
// Pass `compiledSteps` to the precompiled component
// If you do this on server-side or build-time, you can serialize `compiledSteps` into JSON
You can read The Magic In Shiki Magic Move to understand how Shiki Magic Move works.
MIT License © 2023-PRESENT Anthony Fu