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> ) }
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
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
export default function App() { return <h1>Hello World</h1> }
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
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:
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
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
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
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> ) }
To enter the code editing mode, press Enter. To exit the edit mode, press Escape
You are editing the code. To exit the edit mode, press Escape
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.