SvelteKit with the Monaco Editor (2023)

by Daniel Veihelmann, published on 05/18/2023
Latest update: 08/06/2023

Do you want to add a code editor to your SvelteKit app? Good news: There are several nice JavaScript libraries to choose from. Today, we'll be taking a closer look at the Monaco Editor by Microsoft, and how to use it with SvelteKit (including Typescript support).

What is the Monaco Editor?

The Monaco Editor is a popular Javascript code editor. It is also the editor that powers Visual Studio Code. The editor is freely available on Github, so you can integrate it into your own JavaScript project.

The editor features built-in support for multiple languages, editing, linting, theming, diff mode, and much more. Check out Microsoft's interactive playground to learn more.

How-to: Integrating the Monaco Editor with SvelteKit

Importing the Monaco Editor into a project can be tricky, because the library is very large and comes with special trades like internal Web Workers that need to be dealt with.

Let's see how we can integrate the Monaco editor into a SvelteKit project.

As a first step, let's install the Monaco editor itself:

pnpm install -D monaco-editor

With this done, there are two ways we can integrate the editor into our SvelteKit app.

❎ Alternative 1: Directly using Vite (recommended)

We can integrate the Monaco editor directly using Vite and some glue code. I recommend this approach, as it gives you full control and isn't relying on additional dependencies. If this is not an issue to use, alternative 2 might be for you, too.

First, let's create a new file monaco.ts in our project that contains the setup logic for the Monaco editor.

monaco.ts

import * as monaco from 'monaco-editor';

// Import the workers in a production-safe way.
// This is different than in Monaco's documentation for Vite,
// but avoids a weird error ("Unexpected usage") at runtime
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';

self.MonacoEnvironment = {
    getWorker: function (_: string, label: string) {
        switch (label) {
            case 'json':
                return new jsonWorker();
            case 'css':
            case 'scss':
            case 'less':
                return new cssWorker();
            case 'html':
            case 'handlebars':
            case 'razor':
                return new htmlWorker();
            case 'typescript':
            case 'javascript':
                return new tsWorker();
            default:
                return new editorWorker();
        }
    }
};

export default monaco;

As you can see, the Monaco editor comes with several web workers to keep its UI fast. As required by the editor, we define self.MonacoEnvironment and getWorker() so that the web workers will be loaded correctly (both in development and in production mode).

The special import syntax (the ?worker suffix) is described in Vite's documentation and seems to be the only way to avoid some ugly errors on the console later (although everything seems to be working).

Now we can define our page in Svelte:

+page.svelte

<script lang="ts">
    import { onDestroy, onMount } from 'svelte';
    import type * as Monaco from 'monaco-editor/esm/vs/editor/editor.api';

    let editor: Monaco.editor.IStandaloneCodeEditor;
    let monaco: typeof Monaco;
    let editorContainer: HTMLElement;

    onMount(async () => {
        // Import our 'monaco.ts' file here
        // (onMount() will only be executed in the browser, which is what we want)
        monaco = (await import('./monaco')).default;

        // Your monaco instance is ready, let's display some code!
        const editor = monaco.editor.create(editorContainer);
        const model = monaco.editor.createModel(
            "console.log('Hello from Monaco! (the editor, not the city...)')",
            'javascript'
        );
        editor.setModel(model);
    });

    onDestroy(() => {
        monaco?.editor.getModels().forEach((model) => model.dispose());
        editor?.dispose();
    });
</script>

<div>
    <div class="container" bind:this={editorContainer} />
</div>

<style>
    .container {
        width: 100%;
        height: 600px;
    }
</style>

And that's it 🙌🏼

Compared to alternative 2 (see below), this offers the following advantages:

  • No additional dependencies in addition to the editor itself. Thus, you can update the monaco-editor package whenever you want to
  • Full control over what gets loaded when and how
  • No reliance on external systems (no CDN)

❎ Alternative 2: Using @monaco-editor/loader

As an alternative to the aforementioned alternative 1 (which I recommend), you can also load the Monaco editor using the dedicated loader library.

This is less flexible and means an additional dependency for our project, but it is less notchy and requires less code on our side.

Lets install the loader library:

pnpm install -D @monaco-editor/loader

I guess your editor is only truly "powerful" if there is a separate library to load it 😉

Without further ado, lets create our Svelte page. The main difference compared to alternative 1 is in the onMount() method.

Usage in +page.svelte

+page.svelte

<script lang="ts">
    import loader from '@monaco-editor/loader';
    import { onDestroy, onMount } from 'svelte';
    import type * as Monaco from 'monaco-editor/esm/vs/editor/editor.api';

    let editor: Monaco.editor.IStandaloneCodeEditor;
    let monaco: typeof Monaco;
    let editorContainer: HTMLElement;

    onMount(async () => {
        // Remove the next two lines to load the monaco editor from a CDN
        // see https://www.npmjs.com/package/@monaco-editor/loader#config
        const monacoEditor = await import('monaco-editor');
        loader.config({ monaco: monacoEditor.default });

        monaco = await loader.init();

        // Your monaco instance is ready, let's display some code!
        const editor = monaco.editor.create(editorContainer);
        const model = monaco.editor.createModel(
            "console.log('Hello from Monaco! (the editor, not the city...)')",
            'javascript'
        );
        editor.setModel(model);
    });

    onDestroy(() => {
        monaco?.editor.getModels().forEach((model) => model.dispose());
        editor?.dispose();
    });
</script>

<div>
    <div class="container" bind:this={editorContainer} />
</div>

<style>
    .container {
        width: 100%;
        height: 600px;
    }
</style>

And that's it 🙌🏼

Compared to alternative 1, this approach offers the following advantages:

  • Less code that you have to write yourself
  • The possibility to use a content delivery network (CDN), which can reduce your bandwidth requirements (see comment in onMount())

A couple of points I want to note:

  • in the onMount() method, we load the monaco library when our page is initialized in the browser. We load our local version of the editor and don't use the default CDN that the monaco-loader library uses internally (which is jsdelivr).
  • Note that setting the editor up in onMount() also means that there is no server-side rendering (SSR) for the editor.
  • in the onDestroy() method, we clean up the monaco editor state by calling dispose() on all editor models (think: files)
  • The setup described in this post was tested with the following versions: @sveltejs/kit: "v1.23.0", @monaco-editor/loader: "v1.3.3", monaco-editor: "v0.41.0" and vite: v4.4.9

Thanks for reading. Happy coding!

Need to review a complex merge request?

Let's get you some help! Simply paste your URL below:

Works with gitlab github