Back to Blog

Integrating Sandpack with next.js

How I integrated Sandpack into my Next.js blog to create interactive code examples.

July 14, 2025
8 min read

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.

Code Example
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:

Bash
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:

JSX
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:

JSX
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:

JSX
// 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:

JSX
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):

JSX
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:

JSX
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:

JSX
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:

JSX
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:

Code Example
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.

Back to Blog
Loading