Skip to contentVisit my new website at chrisnicholas.dev
Article posted on()

How to Use Sandpack for Code Demos
A guide to building fully customisable code demos in React

Sandpack was released by CodeSandbox earlier this week, a package that takes code demos to the next level, supporting just about every JavaScript framework. In this article I'll be talking about the Sandpack React component, and how to get it customised & running.
Table of contents

Sandpack was released by CodeSandbox earlier this week, a package that takes code demos to the next level, supporting just about every JavaScript framework. In this article I'll be talking about the Sandpack React component, and how to get it customised & running.

import { useState } from 'react'

// Edit me!
export default function App () {
  const [count, setCount] = useState(0)
  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  )
}

What is Sandpack?

Sandpack, and in particular, sandpack-react, is a package that allows you to create interactive code demos that update as you type. Sandpack allows you to:
  • Install NPM dependencies
  • Compile code without an API
  • Display live previews, Jest tests, and console messages
  • Build a custom layout using composable components
  • Create demos for all major JavaScript frameworks
My Sandpack example
Using the react template
export default function App() {
  return <h1>Hello World</h1>
}
DevTools

How does it work

Sandpack works by bundling your code in-browser, without a back end. The preview window is an iframe, embedding a special page that bundles your code, and then displays it:

example.com
My Code Editor
It can compile React
App.js
export default function App () {
  return (
    <div>Welcome to my App!</div>          
  )
}

sandpack.example.com
Welcome to my App!
It can compile Svelte too

This page makes use of service workers to parallelize the bundling process, and to prevent your UI slowing down during as it compiles. Using an iframe prevents some CORS security issues (we wouldn't want any baddies nabbing your users' cookies!), but also it just makes it easier to get up and running—service workers can be finicky to set up.

How do we use it

Amazingly, all you need to do to get this running is import a single component:

npm i @codesandbox/sandpack-react

Pretty nifty. Try changing the template attribute from "react" to "svelte" or "vue" and see it update live! All templates options are listed in the Sandpack API docs under SandpackPredefinedTemplate.

Basic options

The base component also allows a number of options, listed under SandpackOptions in the docs, here's some of them:

Have a play—try uncommenting showNavigator and watch it updated, or call console.log in App.js to see the console.

File format

To add files we need to provide a files object to Sandpack. Files can be written in two formats, string or object, with the key defining the path to the file.

const files = {
  '/App.js': `export default...`,
  
  '/Button.js': {
    code: `export default...`,
    active: true, // Default visible file on load? default `false`
    hidden: false // File visible in tab list? default `true`
  }
}

From this point onwards, we'll exclusively be using the object format to prevent ambiguity. Note that every file's path must always begin with a forward slash (/).

Adding files

Something to remember while using template is that the main file must be called App, otherwise the default demo will show. In this example we're adding two basic files, App.js and Hello.js:

If you open files.js and rename /App.js, you'll see the default demo appearing instead.

Custom theming

We can add some basic themes to Sandpack using CodeSandbox's interactive theme builder. Configure your style, then pass the resulting object to the theme attribute:

It's also possible to import ready-made themes from @codesandbox/sandpack-themes, there's more info regarding this on the Sandpack docs under Themes.

CSS cheatsheet

If you don't plan on completely customising your code demos, you can even make use of a quick CSS cheatsheet I've written:

CSS cheatsheet
/* ===== SandpackProvider ============================= */
/* Overall wrapper */
div.sp-wrapper {}

/* ===== SandpackCodeEditor =========================== */
/* Tab bar */
div.sp-tabs {}

/* Scrollable tab container */
div.sp-tabs-scrollable-container {}

/* Individual tab */
button.sp-tab-button {}

/* Active tab */
button.sp-tab-button[data-active=true] {}

/* Wrapper for components */
div.sp-stack {}

/* Code editor wrapper */
div.sp-code-editor {}

/* Background for code editor */
div.cm-editor {}

/* Code editor content */
div.sp-code-editor div.cm-content {}

/* Line of code */
div.sp-code-editor div.cm-line {}

/* Active line of code */
div.sp-code-editor div.cm-line.cm-activeLine {}

/* Gutter background */
div.sp-code-editor div.cm-gutters {}

/* Line numbers */
div.sp-code-editor div.cm-gutter.cm-lineNumbers {}

/* ===== SandpackPreview ============================== */
/* Preview container */
div.sp-preview-container {}

/* Preview iframe */
iframe.sp-preview-iframe {}

/* Wrapper for buttons in preview container */
div.sp-preview-actions {}

/* Wrapper for loading icon in preview container */
div.sp-loading {}

/* Open in Sandbox button, appears while loading */
button.sp-icon-standalone {}

/* Error box */
div.sp-error {}

/* Error text */
div.sp-error-message {
  white-space: pre-wrap;
}

I'd only advise using this for little changes, otherwise you may see a FOUC (flash of unstyled content) if your customisations conflict with the default styles.

It's that easy

There you go, with just 5 minutes of work you can set up a live code demo with compiling, a live preview, code highlighting, and even a custom theme! Sandpack is magic.

Advanced setups

What we've learnt so far works well, but using a template limits what we can do with Sandpack (plus your main file has to be named App!). Let's have a look at some more complex setups using customSetup.

Options for customSetup

A more complex setup requires us to initialise the component with dependencies, an environment, and an entry file. The React template we used earlier does all this under the hood. This is the customSetup for a React demo:

customSetup={{
  entry: '/index.js',
  environment: 'create-react-app',

  dependencies: {
    react: '^18.2.0',
    'react-dom': '^18.2.0',
    'react-scripts': '^5.0.1',
  },

  files: {
    // ...
  }
}}

To get this working we have to set up React in the entry file (we've called it /index.js above), and give it an HTML to work with. To do this, we can refer to the simplest Sandpack example from earlier, which is handily providing us those files:

If we copy index.js, styles.css, index.html, into a setupFiles object, we can use them ourselves (check inside of setupFiles.js):

Try editing styles.css in the preview window and changing the colour of h1. You can also open setupFiles.js above, and change hidden to true (near the bottom) to see the irrelevant setup tabs disappear.

Make it reusable

To make setting up easier, we can create a basic factory function that will build customSetup for us, and allow us to throw in a couple of options. Don't worry, I've done all the work for you!

Each setup function I've created initialises the framework, and then allows you to define extra dependencies, your files, and the main file (by file name). Here's an example:

customSetup={setupReact({
  dependencies: {
    'date-fns': '^2.27.0'
  },
  files: {
    '/Main.js': // ...
  },
  main: 'Main'
})}

I've made versions for React, Vue, and Svelte. Here they are with working demos:

React

Show React factory and demo
setupReact.js
const indexJs = ({ main }) => `
import React from 'react'
import { createRoot } from 'react-dom/client'
import ${main} from './${main}.js'

const container = document.getElementById('app');
const root = createRoot(container);
root.render(<${main} />);
`

const indexHtml = ({ main }) => `
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>${main}</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
`

const setupReact = (options) => ({
  customSetup: {
    entry: '/index.js',
    environment: 'create-react-app',

    dependencies: {
      react: '^18.2.0',
      'react-dom': '^18.2.0',
      'react-scripts': '^5.0.1',
      ...options.dependencies
    },

    ...options.customSetup
  },

  files: {
    '/index.js': {
      hidden: true,
      code: indexJs(options)
    },

    '/public/index.html': {
      hidden: true,
      code: indexHtml(options)
    },

    ...options.files
  }
})

export default setupReact

Vue

Show Vue factory and demo
setupVue.js
const indexHtml = ({ main }) => `
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <title>${main}</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
`

const indexJs = ({ main }) => `import { createApp } from 'vue'
import ${main} from './${main}.vue'

createApp(${main}).mount('#app')
`

const createVue = (options) => ({
  customSetup: {
    entry: '/index.js',
    environment: 'vue-cli',

    dependencies: {
      'core-js': '^3.6.5',
      vue: '^3.0.0-0',
      '@vue/cli-plugin-babel': '4.5.0',
      ...options.dependencies
    },

    ...options.customSetup
  },

  files: {
    '/index.js': {
       code: indexJs(options),
       hidden: true
    },

    '/index.html': {
       code: indexHtml(options),
       hidden: true
    },

  ...options.files
  }
})

export default createVue

Svelte

Show Svelte factory and demo
setupSvelte.js
const indexJs = ({ main }) => `
import ${main} from './${main}.svelte'

const app = new ${main}({
  target: document.body
})

export default app
`

const indexHtml = ({ main }) => `
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf8" />
    <meta name="viewport" content="width=device-width" />
    <title>${main}</title>
    <link rel="stylesheet" href="public/bundle.css" />
  </head>
  <body>
    <script src="bundle.js"></script>
  </body>
</html>
`

const createSvelte = (options) => ({
  customSetup: {
    entry: '/index.js',
    environment: 'svelte',

    dependencies: {
      svelte: '^3.0.0',
      ...options.dependencies
    },

    ...options.customSetup
  },

  files: {
    '/index.js': {
      code: indexJs(options),
      hidden: true
    },

    '/public/index.html': {
      code: indexHtml(options),
      hidden: true
    },

    ...options.files
  }
})

export default createSvelte

Build your own factory

These factories were based on the Sandpack templates, and there are a few more examples of setups over there (e.g. vanilla JS, React typescript). Take a look there if you'd like to see how to set them up.

You can even take these factories to the next level, like with my components, and wrap them in a component that applies the current website theme (light/dark, sans/serif) to the previews.

Customising the interface

We can go further to customise Sandpack, making use of its components and hooks. In this section, we'll be using a default template for simplicity.

Modular components

Sandpack also allows us to import smaller components that make up the default main component. First, we wrap everything inside SandpackProvider, then we structure it as we like:

Here we've split the code editor & preview window into two columns, placed the file tabs above, and added a custom Open in CodeSandbox link. Try moving SandpackPreview above the tabs instead, and remove the grid styles.

Running tests

One of my favourite components is SandpackTests, which combined with a parcel testing setup, can turn Sandpack into a handy little Jest test runner:

How to run TypeScript tests

Hooks

A number of React hooks are also available to use which allow you to create your own components, for example you can make a custom set of tabs like this:

const { sandpack } = useSandpack()
const { visibleFiles, activeFile, setActiveFile } = sandpack

return (
  <div>
    {visibleFiles.map(name => (
      <button
        key={name}
        onClick={() => setActiveFile(name)}
        data-active={name === activeFile}
      >
        {name}
      </button>
    ))}
  </div>
)

You could even make a custom code editor using whichever package you'd like. In this example I'm making use of a simple textarea:

const { code, updateCode } = useActiveCode()

return (
  <textarea onChange={e => updateCode(e.target.value)} value={code} />
)

And here's a working example using the two components above:

Try uncommenting import './customStyles.css' for a little custom styling.

With a little more work

With a little more work you can get a very nice code demo up and running:

import { useState } from 'react'

// Edit me!
export default function App () {
  const [count, setCount] = useState(0)
  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  )
}

Host the bundler yourself

By default, the bundler page (in the iframe, running the service workers) is hosted on CodeSandbox's server, but you can actually host this static page yourself. To find it, make sure you've installed the package and then look for this directory in your project:

node_modules/@codesandbox/sandpack-client/sandpack

Upload this folder as the root of a different domain name, or as a subdomain, (to prevent CORS vulnerabilities) and then specify the address within SandpackProvider:

<SandpackProvider
  bundlerURL="sandpack.example.com"
  // ...
>

And there you go! Remember that any NPM dependencies you've specified will still be retrieved from the web.

Summary

Sandpack is a fantastic resource for building code demos, allowing for truly customisable components—but this is actually just the beginning. The bundler itself is a powerful tool that I've hardly even had time to mention. For more info, take a look at the Sandpack documentation. Give me a Tweet if this article has been helpful, or if you're using Sandpack to build anything interesting!