DEV Community

Mahesh Odedara
Mahesh Odedara

Posted on • Originally published at heshify.github.io

Redoed #5: Building a Markdown editor with CodeMirror and React

Building Redoed: Markdown editor with CodeMirror and React

Creating a React App with Vite and Bun

For the frontend, we are going to use React. I chose Vite and Bun to set up the React app inside our project’s root directory. The frontend code will live in the frontend folder, which we will later serve using a Go server

To set up the Vite app, I ran:

bun create vite@latest
Enter fullscreen mode Exit fullscreen mode

I am also using Tailwind CSS and ShadCN UI for styling, so I followed their guides to set everything up, including implementing dark mode as guided by ShadCN UI documentation.

Building the Markdown Editor

Now, let's focus on building the Markdown editor. We'll use CodeMirror, a powerful and flexible code editor component for the web. It comes with syntax highlighting, customization options, and smooth React integration.

Installing dependencies

I'll use @uiw/react-codemirror which is CodeMirror component for React. Let's install it using bun along with the theme and other dependencies. We will use react-markdown for previewing and remark-gfm to support Github-flavoured Markdown.

bun add @uiw/codemirror-theme-github @uiw/react-codemirror @codemirror/lang-markdown @codemirror/language-data react-markdown remark-gfm github-markdown-css
Enter fullscreen mode Exit fullscreen mode

Setting Up CodeMirror

Now that the dependencies are installed, Let's create a src/components/markdown-editor.tsx file and bring the dependencies into the project and set up the editor.

import { useCodeMirror, basicSetup, EditorView } from "@uiw/react-codemirror";
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
import { languages } from "@codemirror/language-data";
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
Enter fullscreen mode Exit fullscreen mode

CodeMirror supports syntax highlighting via HighlightStyle. I defined a highlight style for Markdown headings to make them standout.

import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { tags as t } from "@lezer/highlight";

const markdownHighlightStyle = HighlightStyle.define([
  { tag: t.heading1, fontSize: "2em", fontWeight: "bold" },
  { tag: t.heading2, fontSize: "1.75em", fontWeight: "bold" },
  { tag: t.heading3, fontSize: "1.5em", fontWeight: "bold" },
]);
Enter fullscreen mode Exit fullscreen mode

We'll make the editor's background transparent so it blends in with the site's theme.

const myTheme = EditorView.theme({
  "&": {
    backgroundColor: "transparent !important",
  },
});
Enter fullscreen mode Exit fullscreen mode

The Editor Component

The core of the editor is built within a functional React component. It uses useState to store the Markdown content and useCallback to handle text changes. Here, we are also using useTheme hook to get the theme information to make sure the site theme also applies to the editor.

import { useCallback, useState } from "react";
import { useTheme } from "./theme-provider";

function Editor() {
  const { theme } = useTheme();
  const resolvedTheme =
    theme === "system"
      ? window.matchMedia("(prefers-color-scheme: dark)").matches
        ? "dark"
        : "light"
      : theme;

  const [value, setValue] = useState<string>("# Welcome to Redoed!");
  const handleChange = useCallback((val: string) => setValue(val), []);
Enter fullscreen mode Exit fullscreen mode

Initializing CodeMirror

Now that we have the theme, syntax highlighting, and state set up, it's time to put it all together. We'll use useCodeMirror to create the editor and apply everything we've configured so far.

const { setContainer } = useCodeMirror({
  value, // Uses the state we set earlier to store the Markdown content
  height: "90vh", // Sets a fixed height for the editor
  extensions: [
    basicSetup(), // Adds essential features like line numbers and bracket matching
    markdown({
      base: markdownLanguage, // Enables Markdown syntax support
      codeLanguages: languages, // Adds syntax highlighting for code blocks
      addKeymap: true, // Enables useful keyboard shortcuts
    }),
    syntaxHighlighting(markdownHighlightStyle), // Applies our custom Markdown styling
    EditorView.lineWrapping, // Enables line wrapping
    myTheme, // Uses our transparent background theme
  ],
  theme: resolvedTheme === "dark" ? githubDark : githubLight, // Adjusts the theme dynamically
  onChange: handleChange, // Updates the state when the user types
});
Enter fullscreen mode Exit fullscreen mode

The last step is rendering the editor by attaching setContainer to a div, which gives CodeMirror a place to mount itself.

Rendering the Markdown Preview

Now that the editor is set up, it's time to display the rendered Markdown. I'll use ReactMarkdown to convert the Markdown text into HTML and remark-gfm to support GitHub-style extensions like tables and strikethroughs.

import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import "github-markdown-css";

return (
    <div className="w-screen sm:grid sm:grid-cols-2 py-2">
      <div>
        <div ref={setContainer} className="border-r-1" />
      </div>
      <div>
        <div className="h-[90vh] markdown-body p-2 markdown-preview overflow-y-auto scrollbar hidden sm:block !bg-background">
          <ReactMarkdown remarkPlugins={[remarkGfm]}>{value}</ReactMarkdown>
        </div>
      </div>
    </div>
  );
);
Enter fullscreen mode Exit fullscreen mode

This layout keeps the editor and preview side by side on larger screens while keeping the UI clean. Any text typed into the editor instantly reflects in the preview without needing a manual refresh.

Adding a Header component

Let’s also create header.tsx and add this code. For now, it’s just for the show, with none of the buttons working, but it will evolve later.

import { Save, SquarePen } from "lucide-react";
import { ModeToggle } from "./mode-toggle";
import { Button } from "./ui/button";

function Header() {
  return (
    <header className="bg-background flex h-12 w-screen items-center justify-between border px-4 py-2">
      <div className="flex items-center gap-1">
        <Button variant={"outline"} className="cursor-pointer">
          <SquarePen />
        </Button>
        <div className="w-25 sm:w-auto">
          <p className="truncate font-medium">Untitled</p>
        </div>
      </div>

      <div className="flex gap-1">
        <Button className="cursor-pointer" variant={"outline"}>
          <Save />
          Save
        </Button>
        <Button className="cursor-pointer">Login</Button>
        <Button className="cursor-pointer" variant={"secondary"}>
          Sign Up
        </Button>
        <ModeToggle />
      </div>
    </header>
  );
}

export default Header;
Enter fullscreen mode Exit fullscreen mode

Putting Everything Together

Now that we have both the Editor and Header components, let’s bring them into App.tsx.

import Editor from "@/components/markdown-editor";
import Header from "@/components/header";
import { ThemeProvider } from "./components/theme-provider";

function App() {
  return (
    <ThemeProvider>
      <main className="h-fit">
        <Header />
        <Editor />
      </main>
    </ThemeProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

To start the development server, run:

bun run dev
Enter fullscreen mode Exit fullscreen mode

Here's how the Markdown editor looks in action

screenshot

Here's how far we've progressed in Redoed up to this point: GitHub - ui-md-editor branch

Postmark Image

The email service that speaks your language

Whether you code in Ruby, PHP, Python, C#, or Rails, Postmark's robust API libraries make integration a breeze. Plus, bootstrapping your startup? Get 20% off your first three months!

Start free

Top comments (0)

ACI image

ACI.dev: Fully Open-source AI Agent Tool-Use Infra (Composio Alternative)

100% open-source tool-use platform (backend, dev portal, integration library, SDK/MCP) that connects your AI agents to 600+ tools with multi-tenant auth, granular permissions, and access through direct function calling or a unified MCP server.

Check out our GitHub!