While writing my blog posts, I often find myself needing to demonstrate code snippets or interactive examples. This is where Sandpack comes in handy. It allows you to create live code examples that can be embedded directly into your blog posts.
export default function App() { return <h1>Hello world</h1> }
Integrating Sandpack with Next.js is pretty straightforward, but modifying Sandpack to fit my specific needs was a bit challenging.
I wanted Sandpack to show my code examples on the right side of the editor, and wanted the console and preview window to be on the left side.
The Basic Setup
First, I installed the necessary dependencies:
npm install @codesandbox/sandpack-react prettier
The basic Sandpack integration is surprisingly simple. You just wrap your content in a SandpackProvider
and add the components you need:
import {
SandpackProvider,
SandpackCodeEditor,
SandpackPreview,
SandpackLayout,
} from "@codesandbox/sandpack-react";
export default function BasicSandpack() {
return (
<SandpackProvider template="react" theme="dark">
<SandpackLayout>
<SandpackCodeEditor />
<SandpackPreview />
</SandpackLayout>
</SandpackProvider>
);
}
But I wanted something more sophisticated.
Custom Layout with Tabbed Output
The default Sandpack layout shows the preview and console in separate panels. I wanted a tabbed interface that would save space and provide a cleaner experience:
function TabbedOutput() {
const [activeTab, setActiveTab] =
(useState < "preview") | ("console" > "preview");
return (
<div className="flex w-1/2 min-w-0 flex-col">
<div className="flex border-b border-zinc-500 bg-zinc-600">
<button
onClick={() => setActiveTab("preview")}
className={`${
activeTab === "preview"
? "border-b-2 border-blue-400 bg-zinc-700 text-white"
: "hover:bg-zinc-650 text-gray-300 hover:text-white"
} flex-1 px-4 py-2 text-sm font-medium transition-colors duration-200`}
>
Preview
</button>
<button
onClick={() => setActiveTab("console")}
className={`${
activeTab === "console"
? "border-b-2 border-blue-400 bg-zinc-700 text-white"
: "hover:bg-zinc-650 text-gray-300 hover:text-white"
} flex-1 px-4 py-2 text-sm font-medium transition-colors duration-200`}
>
Console
</button>
</div>
<div className="min-h-0 flex-1">
<SandpackPreview
style={{
height: activeTab === "preview" ? "300px" : "0",
}}
/>
<SandpackConsole
style={{
height: activeTab === "console" ? "300px" : "0",
}}
/>
</div>
</div>
);
}
This creates a much cleaner interface where users can switch between seeing the live preview and the console output.
Adding Prettier Integration
One of the most challenging parts was integrating Prettier for code formatting. I wanted users to be able to format their code with a simple button click or keyboard shortcut.
The Challenge
The main issue was that Prettier's browser-compatible imports return Promises, not the formatted strings directly. This caught me off guard initially:
// This doesn't work as expected
const formatted = prettier.format(code, { parser: "babel" });
console.log(formatted); // Outputs: Promise {<fulfilled>: "formatted code"}
The Solution
I had to make the formatting function async and properly await the Prettier calls:
const prettifyCode = async () => {
const activeFile = sandpack.files[sandpack.activeFile];
if (!activeFile) return;
const currentCode = activeFile.code;
try {
const fileExtension = sandpack.activeFile.split(".").pop()?.toLowerCase();
let formattedCode = currentCode;
if (fileExtension === "scss" || fileExtension === "css") {
formattedCode = await prettier.format(currentCode, {
parser: "scss",
plugins: [parserSCSS],
});
} else {
formattedCode = await prettier.format(currentCode, {
parser:
fileExtension === "ts" || fileExtension === "tsx"
? "typescript"
: "babel",
plugins: [parserBabel, parserTS, parserHTML, prettierPluginEstree],
});
}
setError(false);
setSuccess(true);
updateCurrentFile(formattedCode);
} catch (error) {
setError(true);
console.error("Prettier formatting error:", error);
}
};
I also added keyboard shortcut support so users can format with Cmd+S
(or Ctrl+S
on Windows):
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
event.preventDefault();
prettifyCode();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [sandpack.files, sandpack.activeFile]);
Adding Delightful Animations
To make the experience more engaging, I added GSAP-powered animations to the toolbar buttons. (Thanks to the inspiration from Josh Comeau's blog!)
Sparkle Animation on Format
When users click the format button, a burst of sparkles emanates from the button:
const createSparkleAnimation = () => {
if (!buttonRef.current || !sparklesRef.current) return;
const sparkleCount = 8;
const sparkles: HTMLElement[] = [];
// Clear existing sparkles
sparklesRef.current.innerHTML = '';
for (let i = 0; i < sparkleCount; i++) {
const sparkle = document.createElement('div');
sparkle.innerHTML = '✨';
sparkle.style.position = 'absolute';
sparkle.style.fontSize = '8px';
sparkle.style.pointerEvents = 'none';
sparkle.style.zIndex = '1000';
sparkle.style.left = '50%';
sparkle.style.top = '50%';
sparkle.style.transform = 'translate(-50%, -50%)';
sparklesRef.current.appendChild(sparkle);
sparkles.push(sparkle);
}
// Animate sparkles
sparkles.forEach((sparkle, index) => {
const angle = (360 / sparkleCount) * index;
const distance = 20 + Math.random() * 15;
const duration = 0.6 + Math.random() * 0.4;
gsap.set(sparkle, {
rotation: angle,
scale: 0,
opacity: 1,
});
gsap.to(sparkle, {
x: Math.cos((angle * Math.PI) / 180) * distance,
y: Math.sin((angle * Math.PI) / 180) * distance,
scale: 0.8 + Math.random() * 0.4,
rotation: angle + 180,
opacity: 0,
duration: duration,
ease: 'power2.out',
onComplete: () => {
sparkle.remove();
},
});
});
};
Hover Animations
I also added subtle jiggle animations on hover to give the magic wand icon more personality:
const handleMouseEnter = () => {
gsap.to(buttonRef.current, {
rotation: 15,
duration: 0.08,
ease: "power2.inOut",
yoyo: true,
repeat: 3,
onComplete: () => {
gsap.to(buttonRef.current, {
rotation: 0,
duration: 0.1,
ease: "power2.inOut",
});
},
});
};
Custom Toolbar Integration
Everything comes together in a custom title bar that houses all the controls:
function TitleBar({ title = "Code Example" }: { title?: string }) {
const { sandpack } = useSandpack();
const { resetAllFiles } = sandpack;
const { prettier } = useIsPrettier();
const { error, success, prettifyCode } = usePrettier();
return (
<div className="mb-0 flex items-center justify-between bg-zinc-700 px-3 py-2 sm:rounded-t-lg">
<span className="text-sm text-white">{title}</span>
<div className="flex w-auto items-center space-x-4">
{prettier && (
<button className="flex-none">
<FormatIcon
className="h-4 w-4"
error={error}
success={success}
onClick={prettifyCode}
/>
</button>
)}
<button className="flex-none" onClick={() => resetAllFiles()}>
<ResetIcon className="h-4 w-4" />
</button>
</div>
</div>
);
}
Try It Out!
Here's the final result - a fully interactive code playground with formatting, animations, and a clean interface:
import {useState} from 'react' export default function App() { const [count, setCount] = useState(0); return ( <div style={{ padding: "20px", textAlign: "center" }}> <h1>Interactive Counter</h1> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> <button onClick={() => setCount(count - 1)}>Decrement</button> </div> ); }
Try clicking the magic wand icon to format the code - you'll see the sparkle animation in action! You can also use Cmd+S
(or Ctrl+S
) to format.
Conclusion
If you're building a technical blog or documentation site, I highly recommend exploring Sandpack. With some customization, it can become a powerful tool for creating engaging, interactive content.