How To Add Monaco Editor to a Next.js app

Bottom Line Up Front

I use a slightly modified version of the steps mentioned in this GitHub comment. Modifications were necessary because I use TailwindCSS with Next.js.


Monaco Editor is [the open source editor] ( used in VS Code, which itself is open source. I used to write my blogposts in VS Code, and as I make my own CMS, I wanted to have all the familiar trappings of Monaco to help me out while I write.


However there are some issues we have to deal with:

  • Monaco is framework agnostic, so it requires writing some React bindings.
  • Monaco is written for a desktop Electron app, not for a server-side rendered web app.
    • This is solved by using import dynamic from "next/dynamic" and making Monaco a dynamic import.
  • Monaco also wants to offload syntax highlighting to web workers, and we need to figure that out
  • Next.js doesn’t want any dependencies importing CSS from within node_modules, as this assumes a bundler and loader setup (e.g. webpack) and can have unintentional global CSS side effects (all global CSS is intended to be in _app.js).

We can solve this with a solution worked out by Elliot Hesp on GitHub and a config from Joe Haddad of the Next.js team.


The solution I use is informed by my usage of Tailwind CSS, which requires a recent version of PostCSS, which @zeit/next-css only has at 3.0 (because it is deprecated and not maintained).

I also use TypeScript, which introduces a small wrinkle, because Monaco Editor attaches a MonacoEnvironment global on the window object - I just @ts-ignore it.

// next.config.js

const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
const withTM = require("next-transpile-modules")([
  // `monaco-editor` isn't published to npm correctly: it includes both CSS
  // imports and non-Node friendly syntax, so it needs to be compiled.

module.exports = withTM({
  webpack: config => {
    const rule = config.module.rules
      .find(rule => rule.oneOf)
        r =>
          // Find the global CSS loader
          r.issuer && r.issuer.include && r.issuer.include.includes("_app")
    if (rule) {
      rule.issuer.include = [
        // Allow `monaco-editor` to import global CSS:

      new MonacoWebpackPlugin({
        languages: [
        filename: "static/[name].worker.js"
    return config;

and then in your Next.js app code:

import React from "react";
// etc

import dynamic from "next/dynamic";
const MonacoEditor = dynamic(import("react-monaco-editor"), { ssr: false });

function App() {
  const [postBody, setPostBody] = React.useState("");
  // etc
  return (<div>
  {/* etc */}
      editorDidMount={() => {
        // @ts-ignore
        window.MonacoEnvironment.getWorkerUrl = (
          _moduleId: string,
          label: string
        ) => {
          if (label === "json")
            return "_next/static/json.worker.js";
          if (label === "css")
            return "_next/static/css.worker.js";
          if (label === "html")
            return "_next/static/html.worker.js";
          if (
            label === "typescript" ||
            label === "javascript"
            return "_next/static/ts.worker.js";
          return "_next/static/editor.worker.js";
        minimap: {
          enabled: false

Since I’m using Tailwind, I’m also using PostCSS, which also tries to eliminate Monaco’s CSS. You have to tell it to ignore that:

// postcss.config.js
const purgecss = [
    content: ["./components/**/*.tsx", "./pages/**/*.tsx"],
    css: [],
    whitelistPatternsChildren: [/monaco-editor/], // so it handles .monaco-editor .foo .bar
    defaultExtractor: content => content.match(/[\w-/.:]+(?<!:)/g) || []

Catch up on the CMS LiveStream!

Leave a reaction if you liked this post! 🧡
Loading comments...

Subscribe to the newsletter

Join >10,000 subscribers getting occasional updates on new posts and projects!

I also write an AI newsletter and a DevRel/DevTools newsletter.

Latest Posts

Search and see all content