Destiner's notes

Using Sandpack Vue Components

5 February 2022

This guide provides an overview of the sandpack-vue package.

sandpack-vue provides Vue components that wrap Sandpack and add commonly used utilities. It provides 2 components: Sandbox and Editor. We will see below how they can be used.

Component layout

Simple sandbox

We will start by creating a component that will render a static HTML page.

First, import the Sandbox component. You’ll also need to import style.css to import necessary component styling.

import { Sandbox } from 'sandpack-vue';
import 'sandpack-vue/style.css';

Second, provide files to be rendered, any project dependencies, and an entry point:

const info = {
  files: {
    '/index.html': {
      code: `<!DOCTYPE html>
      <html>
      <body>
      <h1>Hello Sandpack!</h1>
      <div id="app"></div>
      <script src="src/index.ts" />
      </body>
      </html>`,
    },
    '/index.js': {
      code: `import { v4 as uuidv4 } from 'uuid';
      document.getElementById("app").innerHTML = \`
      <div>
      $\{uuidv4()}
      </div>\``,
    },
  },
  entry: '/index.js',
  dependencies: {
    uuid: 'latest',
  },
};

Use the component:

<template>
  <Sandbox class="sandbox" :info="info" />
</template>

Optionally, you can style how the output component would look like by adding style rules to the Sandbox element:

.sandbox {
  width: 800px;
  height: 400px;
  border: 1px solid #2c3e50;
  border-radius: 4px;
}

Editor support

While rendering a static page might be useful in some contexts, often we want to have a dynamic sandbox where the rendered output depends on user input. This input is usually provided by the text editor that allows changing the code of one or multiple files.

First, import both Sandbox and Editor components. You can also import the File type if you’re using Typescript:

import { Editor, File, Sandbox } from 'sandpack-vue';
import 'sandpack-vue/style.css';

Second, create the code ref value that will store the file content:

const code = ref(`<!DOCTYPE html>
<html>
<body>
  <h1>Hello Sandpack!</h1>
</body>
</html>`);

Next, create a handler function that will update code ref based on user input:

function handleCodeChange(_index: number, value: string) {
  code.value = value;
}

Then, create the files computed to pass it to Editor as a prop:

const files = computed<File[]>(() => [
  {
    value: code.value,
    name: 'index.html',
    type: 'html',
    editable: true,
    visible: false,
  },
]);

Create an info computed to be passed to the Sandbox component:

const info = computed(() => {
  return {
    files: {
      '/index.html': {
        code: code.value,
      },
    },
    entry: '/index.html',
    dependencies: {},
  };
});

Finally, add both Editor and Sandbox components to the template:

<template>
  <div class="exercise">
    <Editor class="editor" :files="files" @change="handleCodeChange" />
    <Sandbox class="sandbox" :info="info" :options="options" />
  </div>
</template>

Multi-file editor

Having an editor available to update a single file is neat, but usually, you want to have multiple files. While providing a full-blown editable file system inside a browser is out of the scope of this article, I will show you how to make the Editor component render a file selector to toggle between files.

To enable a multi-file editor, first, you’d need to make files a ref instead of computed. We can also remove the code ref and rely on files to store file contents:

const files = ref<File[]>([
  {
    value: `<!DOCTYPE html>
<!-- First file will always be active, even if it's hidden. -->
<html>
<body>
  <h1>Hello from Sandpack!</h1>
  <p>This file is hidden</p>
</body>
</html>`,
    name: 'hidden.html',
    type: 'html',
    editable: true,
    visible: false,
  },
  {
    value: `<!DOCTYPE html>
<!-- Once hidden file is not active, it's impossible to open it. -->
<html>
<body>
  <h1>Hello Sandpack!</h1>
  <p>This file is visible</p>
</body>
</html>`,
    name: 'index.html',
    type: 'html',
    editable: true,
    visible: true,
  },
]);

We also need to update our handler to change files based on index:

function handleCodeChange(index: number, value: string) {
  files.value[index].value = value;
}

Finally, let’s update an info computed to fetch file contents from files ref:

const info = computed(() => {
  return {
    files: Object.fromEntries(
      files.value.map((file) => ['/' + file.name, { code: file.value }]),
    ),
    entry: '/index.html',
    dependencies: {},
  };
});

Editor theming

You can theme the editor component by providing a custom theme. Themes are compatible with Sandpack for React.

You can choose from one of the themes bundled with the package:

import { getPredefinedTheme } from 'sandpack-vue';

const theme = getPredefinedTheme('sandpack-dark');

And use it in the Editor component:

<template>
  <div class="exercise">
    <Editor
      class="editor"
      :files="files"
      :theme="theme"
      @change="handleCodeChange"
    />
    <Sandbox class="sandbox" :info="info" :options="options" />
  </div>
</template>

You can see all built-in themes here.

Alternatively, you can create a custom theme:

const theme: SandpackTheme = {
  palette: {
    activeText: '#de904d',
    defaultText: '#bababa',
    inactiveText: '#979797',
    activeBackground: '#2a2a2a',
    defaultBackground: '#343434',
    inputBackground: '#2e2e2e',
    accent: '#de904d',
    errorBackground: '#dac1fb',
    errorForeground: '#b08df8',
  },
  syntax: {
    plain: '#f0fdaf',
    comment: {
      color: '#757575',
      fontStyle: 'italic',
    },
    keyword: '#e5fd78',
    tag: '#f0fdaf',
    punctuation: '#ffffff',
    definition: '#eeeeee',
    property: '#e0a571',
    static: '#ffffff',
    string: '#dafecf',
  },
  typography: {
    bodyFont:
      'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
    monoFont:
      '"Fira Code", "Fira Mono", "DejaVu Sans Mono", Menlo, Consolas, "Liberation Mono", Monaco, "Lucida Console", monospace',
    fontSize: '14px',
    lineHeight: '1.6',
  },
};