I Built `Sandpack Embedder`: Turn `Code Snippets` into `Live Playgrounds` Without Touching Your CMS
Frustrated with the complexity of adding interactive code playgrounds to CMS-generated content? I built Sandpack Embedder to solve this exact problem. Here's why and how it works.
A few days ago, I ran into a frustrating problem. I was writing technical tutorials on my Ghost blog, and I wanted to add interactive code examples—you know, the kind where readers can actually edit and run the code right in the browser.
Sandpack seemed perfect for this. It's the same technology CodeSandbox uses, and it creates beautiful, functional playgrounds. But there was one big problem: how do I actually get Sandpack into my Ghost posts?
Let me show you what I wanted to achieve. Here's an interactive playground running directly in my Ghost blog:
<sandpack
template="react"
custom-setup='{
"dependencies": {
"motion": "12.23.24"
}
}'
options='{
"editorHeight": 500
}'
>
```js /App.js
/**
* Animate state
* An example of animating in response to state changes using Motion for React.
* Made by Matt Perry https://twitter.com/mattgperry
* https://examples.motion.dev/react/state-updates
*/
"use client"
import * as motion from "motion/react-client"
import { useState } from "react"
import StyleSheet from "./StyleSheet"
import Input from "./Input"
export default function StateAnimations() {
const [x, setX] = useState(0)
const [y, setY] = useState(0)
const [rotate, setRotate] = useState(0)
return (
<div id="example">
<div>
<motion.div
className="box"
animate={{ x, y, rotate }}
transition={{ type: "spring" }}
/>
</div>
<div className="inputs">
<Input value={x} set={setX}>
x
</Input>
<Input value={y} set={setY}>
y
</Input>
<Input value={rotate} set={setRotate} min={-180} max={180}>
rotate
</Input>
</div>
<StyleSheet />
</div>
)
}
```
```js /Input.js
export default function Input({ value, children, set, min = -200, max = 200 }) {
return (
<label>
<code>{children}</code>
<input
value={value}
type="range"
min={min}
max={max}
onChange={(e) => set(parseFloat(e.target.value))}
/>
<input
type="number"
value={value}
min={min}
max={max}
onChange={(e) => set(parseFloat(e.target.value) || 0)}
/>
</label>
)
}
```
```js /StyleSheet.js
export default function StyleSheet() {
return (
<style>{`
#example .box {
width: 200px;
height: 200px;
border-radius: 20px;
border: 5px dotted #ff0088;
pointer-events: none;
}
#example {
display: flex;
align-items: center;
}
#example input {
accent-color: #ff0088;
font-family: "Azeret Mono", monospace;
}
#example .inputs {
display: flex;
flex-direction: column;
padding-left: 50px;
}
#example label {
display: flex;
align-items: center;
margin: 10px 0;
}
#example label code {
width: 100px;
}
#example input[type="number"] {
border: 0;
border-bottom: 1px dotted #ff0088;
color: #ff0088;
margin-left: 10px;
}
#example input[type="number"]:focus {
outline: none;
border-bottom: 2px solid #ff0088;
}
#example input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
}
input[type='range']::-webkit-slider-runnable-track {
height: 10px;
-webkit-appearance: none;
background: #0b1011;
border: 1px solid #1d2628;
border-radius: 10px;
margin-top: -1px;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
height: 20px;
width: 20px;
border-radius: 50%;
background: #ff0088;
top: -4px;
position: relative;
}
`}</style>
)
}
```
</sandpack>Pretty cool, right? Readers can edit the code and see changes instantly. But getting this to work wasn't straightforward. Here's the story of how I built Sandpack Embedder to solve this problem.
The Problem
Ghost uses the Koenig editor, which is great for writing but doesn’t support plugins or custom embeds like MDX. I could probably hack something together by modifying Ghost's core, but that felt... wrong. Every time Ghost updates, I'd have to maintain those changes. Plus, what if I wanted to switch CMSs later?
At first, I found a workaround. I'd manually inject Sandpack into each post using HTML block:
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@19.2.0",
"react-dom/client": "https://esm.sh/react-dom@19.2.0/client",
"@codesandbox/sandpack-react": "https://esm.sh/@codesandbox/sandpack-react@2.20.0"
}
}
</script>
<div id="sandpack-playground"></div>
<script type="module" src="https://esm.sh/tsx"></script>
<script type="text/babel">
import React from "react";
import { createRoot } from "react-dom/client";
import { Sandpack } from "@codesandbox/sandpack-react";
const root = createRoot(document.getElementById("sandpack-playground"));
root.render(<Sandpack template="react" />);
</script>
It worked! But... I had to copy-paste this entire block for every single playground. Want to show three different examples in one article? That's three sets of three mount points, three script blocks. It became unmaintainable fast.
I needed something better—a way to write code snippets naturally and have them transform into playgrounds automatically.
The Solution: Sandpack Embedder
So I built Sandpack Embedder. The idea is simple: write your code blocks in any format your CMS supports, and let JavaScript do the heavy lifting on the client side.
Here's how it works:
Write Code Blocks in Your CMS
In Ghost (or any CMS), I just write regular code blocks with a special wrapper:
<sandpack template="react">
```js /App.js
export default function App() {
return <h1>Hello world</h1>
}
```
</sandpack>
The CMS saves this as escaped HTML in the database. Nothing special. No plugins needed.
2. Magic Happens on Page Load
Add a single script tag to your theme, or place it in Code Injection if your CMS supports it:
<script type="module">
import { SandpackEmbedder } from "https://esm.sh/@rizalibnu/sandpack-embedder";
new SandpackEmbedder({
codeSelector: "pre > code.language-sandpack",
}).load();
</script>
That's it. The embedder:
- Finds all code blocks with the
pre > code.language-sandpackselector - Parses the
<sandpack>markup - Extracts the code files
- Renders a live
Sandpackplayground
3. Readers Get Interactive Playgrounds
Instead of static code, readers see a fully functional editor and preview. They can edit the code, see results instantly, and even install dependencies.
Why This Approach?
No CMS modifications needed. Everything happens in the browser after the page loads. Your CMS just serves plain HTML—exactly what it's good at.
Works everywhere. Ghost, WordPress, custom CMSs, static site generators—if it outputs HTML, it works.
Easy to customize. Want dark mode? Custom themes? Different layouts? Just pass options:
new SandpackEmbedder({
theme: "dark",
customThemes: { myTheme },
injectPosition: "after"
}).load();
No build step required. Load it from a CDN and you're done. No npm, no webpack, no configuration hell.
Real-World Example
Here's what I actually write in Ghost's code block:
<sandpack template="react">
```js /App.js
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
```
</sandpack>
<sandpack template="react">
```js /App.js
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
```
</sandpack>
And readers get a working React playground. They can click the button, edit the code, see it update live. All without me touching Ghost's core or maintaining custom plugins.
Beyond Basic Usage
The embedder supports some pretty cool features I built while using it myself:
Multiple Files
<sandpack>
```js /App.js active
export default function App() {
return <h1>Hello</h1>
}
```
```css /styles.css
h1 { color: blue; }
```
```js /utils.js hidden
export const helper = () => {}
```
</sandpack>
<sandpack>
```js /App.js active
export default function App() {
return <h1>Hello</h1>
}
```
```css /styles.css
h1 { color: blue; }
```
```js /utils.js hidden
export const helper = () => {}
```
</sandpack>
Built-in Presets for Documentation
Sometimes you don't need the full editor + preview. I added two presets:
Preview-only (great for demos):
<preview template="react">
```js /App.js
export default function App() {
return <h1>Just show the result!</h1>
}
```
</preview>
<preview template="react">
```js /App.js
export default function App() {
return <h1>Just show the result!</h1>
}
```
</preview>
Code viewer (great for readonly examples):
<code-viewer template="react" options="{'viewerHeight': 150}">
```js /App.js
// Read-only code display
export default function App() {
return <h1>No editing needed</h1>
}
```
```js /console.js
console.log("Terima kasih")
```
</code-viewer><code-viewer template="react" options="{'viewerHeight': 150}">
```js /App.js
// Read-only code display
export default function App() {
return <h1>No editing needed</h1>
}
```
```js /console.js
console.log("Terima kasih")
```
</code-viewer>These presets are perfect for documentation where you want to show "here's what it looks like" or "here's the code" without the full interactive playground.
Custom Injection Points
Control exactly where playgrounds appear:
new SandpackEmbedder({
injectTarget: ".article-content",
injectPosition: "before"
});
Theme Switching
const embedder = new SandpackEmbedder({ theme: "light" });
embedder.load();
// Later...
embedder.updateTheme("dark");
Custom Theme
Want to use themes from the @codesandbox/sandpack-themes package? Just import and pass them:
import { gruvboxDark } from "@codesandbox/sandpack-themes";
const sandpack = new SandpackEmbedder({
theme: gruvboxDark
}).load();
<sandpack theme="gruvboxDark"></sandpack>Custom Themes Mapping
You can create a theme registry and switch between them by name:
import { amethyst, aquaBlue } from "@codesandbox/sandpack-themes";
const sandpack = new SandpackEmbedder({
customThemes: { amethyst, aquaBlue, neoCyan },
theme: "amethyst"
}).load();
// Switch themes by name
sandpack.updateTheme("aquaBlue");
This is perfect for adding theme switchers to your blog without reloading the page.
Custom Components
Need complete control over the Sandpack layout? You can replace the default component or add new ones:
<script type="importmap">
{
"imports": {
"@codesandbox/sandpack-react": "https://esm.sh/@codesandbox/sandpack-react@2.20.0",
"@rizalibnu/sandpack-embedder": "https://esm.sh/@rizalibnu/sandpack-embedder"
}
}
</script>
<script type="module" src="https://esm.sh/tsx"></script>
<script type="text/babel">
import { SandpackEmbedder } from "@rizalibnu/sandpack-embedder";
import {
SandpackProvider,
SandpackLayout,
SandpackPreview,
SandpackCodeEditor
} from "@codesandbox/sandpack-react";
function CustomSandpack(props) {
return (
<SandpackProvider {...props}>
<SandpackLayout>
<SandpackCodeEditor />
<SandpackPreview />
</SandpackLayout>
</SandpackProvider>
)
};
function VuePack(props) {
return <Sandpack {...props} template="vue" />;
}
new SandpackEmbedder({
customComponents: { sandpack: CustomSandpack, vuepack: VuePack },
}).load();
</script>
Now you can use <vuepack> or any custom tag in your markdown, and it'll render with your custom component.
Learning from Building This
Building Sandpack Embedder taught me a few things:
- Sometimes the best solution is outside your stack. Instead of fighting my CMS, I worked around it.
- Client-side processing isn't always bad. Yes, this adds JavaScript. But it also means zero server changes and instant updates.
- Libraries should be flexible. By making everything configurable—selectors, injection points, themes—it works for more use cases than I originally imagined.
Try It Yourself
If you're struggling with the same problem I had—wanting interactive code examples without CMS surgery—give Sandpack Embedder a try:
npm install @rizalibnu/sandpack-embedder
Or just load it from a CDN:
<script type="module">
import { SandpackEmbedder } from "https://esm.sh/@rizalibnu/sandpack-embedder";
new SandpackEmbedder().load();
</script>
What's Next?
I'm using this on my own blog now, and it's working great. But I'd love to hear how you use it. Found a cool use case? Built a custom component? Hit an edge case? Let me know!
The code is open source on GitHub. Contributions, issues, and feedback are all welcome.
TL;DR: I got tired of fighting my CMS to add interactive code playgrounds, so I built a library that does it client-side. Write code blocks in your editor, add one script tag, and boom—live Sandpack playgrounds. No CMS modifications required.
Happy coding! 🚀