Building a React code editor and syntax highlighter from scratch

📅 2 years ago 🕒 12 min read 🙎‍♂️ by Madza

Building a React code editor and syntax highlighter from scratch

Long gone are the days when developers coded in Notepad and blogs displayed the code blocks using just HTML. Highlighted code is so much more pleasing to the eye and way easier to read.

In this tutorial, we will create a React code editor and Syntax highlighter, so you can type in your code and see how it gets highlighted. We will also provide interactivity, meaning users will be able to switch between multiple languages and themes.

The source code will be available here, for reference.

Wireframing the app

First, let's create a simple wireframe to design the layout of the consisting components.

Wireframe

The whole app will reside in the App, which will be the main wrapper for our application.

Inside the App there will be ControlsBox and PanelsBox components.

ControlsBox will further include two Dropdown components. One will be for selecting the input language, and the other for selecting the theme of the highlighting.

Setting up the project

To create a project boilerplate, we will be using Create React app, which will set up a fully configured React project in a minute or less.

To do that, open your terminal and run the following command:

npx create-react-app syntax-highlighter

Then switch to the newly created folder by running cd syntax-highlighter and start the React development server by running npm start.

This should automatically open up your browser and you should be presented with a React default app on port 3000.

Open the src folder and remove all the files except App.js, App.css, index.js, and index.css. Then remove the content in each of those, as we will re-write each file entirely from scratch.

Creating the base

First, we will create the base structure of our project to build upon.

Let's start with index.js file, which will render our app. Open it up and include the following code:

import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

In order to be able to render, we first imported ReactDOM component. Then we imported an extended stylesheet to style the base. Finally, we imported the App component and set it up, so that it will be rendered in the root element inside the DOM tree.

Then open index.css file and include the following styles:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  width: 100vw;
  min-height: 100vh;
  font-family: sans-serif;
  background-color: #ffdee9;
  background-image: linear-gradient(0deg, #ffdee9 0%, #b5fffc 100%);
}

We first created the reset rules for margin, padding, and box-sizing, so we do not have to worry about the default browser values for these later. It's a common practice and recommended for any project you ever build from scratch.

We also created specific rules for body, so that it always fills the entire viewport of the screen. We also set a particular font-family and a gradient background.

Then open the App.js, where all the logic of our app will live. Include the following code:

import "./App.css";

export default function App() {
  return (
    <div className="App">
      <div className="ControlsBox"></div>
      <div className="PanelsBox"></div>
    </div>
  );
}

First, we imported an external stylesheet for App.js.

Then we created an App function, which we will be rendered in the previously created index.js. Inside it, we created an App div element, which will be the main wrapper for our app. Furthermore, inside the App wrapper there will be ControlsBox and PanelsBox components.

Then open App.css file and add the following styles:

.App {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.ControlsBox {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
}

.PanelsBox {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
  gap: 20px;
  margin-top: 20px;
}

First, we made sure that the App wrapper never exceeds specific width. We also centered it in the viewport and added the padding inside it.

For the ControlsBox children we set the grid layout with two columns, each of the same width. We also added a gap between both columns.

The PanelsBox children will also use a grid layout with two columns and a gap between them. The layout will automatically switch to one column if the width of the children is less than 400px, meaning the included Editor and Highlighter components will be shown below each other. To separate PanelsBox from ControlsBox we added a margin on the top.

Setting the states

There will be user interaction by selecting the languages and themes, so the data will change. To display them properly on the screen, we will need to store them into the state variables.

For that, we will use React built-in useState hook, which is a standard way of handling this in React ecosystem.

Open the App.js and add the following code:

import React, { useState } from "react";
import "./App.css";

export default function App() {
  const [input, setInput] = useState("");
  const [language, setLanguage] = useState("");
  const [theme, setTheme] = useState("");

  return (
    <div className="App">
      <div className="ControlsBox"></div>
      <div className="PanelsBox"></div>
    </div>
  );
}

First, we imported the React useState hook and then we included input, language, and theme variables inside the App function.

The input will keep track of the input user has written in the Editor, the language will track the programming language user has selected, and the theme will track which highlight theme the user has selected.

Creating the components

To divide the building blocks of the app from the app logic we will create several components that we will later import in the App.js.

We will create a separate folder in the project's root called components and create separate JS and CSS files for Dropdown, Editor, and Highlighter components.

You can create the files manually, or you can use the terminal command mkdir components && cd components && touch Dropdown.js Dropdown.css Editor.js Editor.css Highlighter.js Highlighter.css to save time.

Dropdown

We will use Dropdown component for both language and theme selection. The only variable that will change will be the data we will pass in.

Open the Dropdown.js file and add the following code:

import "./Dropdown.css";

export const Dropdown = ({ defaultTheme, onChange, data }) => {
  return (
    <select className="select" defaultValue={defaultTheme} onChange={onChange}>
      {Object.keys(data)
        .sort()
        .map((theme, index) => {
          return (
            <option key={index} value={theme}>
              {theme}
            </option>
          );
        })}
    </select>
  );
};

We first imported the external stylesheet for Dropdown.js.

We used the select element and then looped through the data prop we will receive from the App to display the available theme options. We sorted the options in alphabetical order.

We also used the defaultProp so we can later set up the default theme option shown on the initial launch as well as onChange prop, so we later have control of what happens when the user selects a particular theme.

Then switch to the DropDown.css file and add the following styles:

.select {
  height: "100px";
  border: none;
  border-radius: 5px;
  padding: 5px 0;
  background-color: #ffffff;
  width: 100%;
}

For the Select component we set the specific height, removed the default border, rounded the corners, added the padding inside, set the background to white and made sure it uses all the available space of the parent horizontally.

Editor

The Editor component will be the text area, where the user will enter the code.

Open the Editor.js file and add the following code:

import "./Editor.css";

export const Editor = ({ placeHolder, onChange, onKeyDown }) => {
  return (
    <textarea
      className="editor"
      placeholder={placeHolder}
      onChange={onChange}
    ></textarea>
  );
};

We first imported the external stylesheet for Editor.js.

Then we returned the textarea element and included the placeholder prop that will display the placeholder value on the initial launch. We also included the onChange prop so we later have control of what happens when the user types in the code.

Let's add some styling to the Editor component. Open Editor.css and include the following styles:

.editor {
  border: none;
  min-height: 300px;
  padding: 10px;
  resize: none;
}

For the Editor component we removed the default border, set the minimum height, and added padding.

We also made sure the editor block is not manually resizable by the user. It will still automatically adjust its height based on the content user has typed in.

Highlighter

In order to highlight the code bocks we will use react-syntax-highlighter package. To install it, run the following command on your terminal:

npm install react-syntax-highlighter

Then open the Highlighter.js file and include the following code:

import SyntaxHighlighter from "react-syntax-highlighter";
import "./Highlighter.css";

export const Highlighter = ({ language, theme, children }) => {
  return (
    <SyntaxHighlighter
      language={language}
      style={theme}
      className="highlighter"
    >
      {children}
    </SyntaxHighlighter>
  );
};

We first imported the SyntaxHighlighter component, then imported an external stylesheet for Highlighter.js.

The SyntaxHighlighter required language and style. We will pass those in once we import the Highlighter into App.js.

Next open Highlighter.css file and add the folowing style rule:

.highlighter {
  min-height: 300px;
}

This will ensure that the Highlighter component always uses minimal height, which will be useful if there is no content (to avoid the component from auto-shrinking).

Creating the app logic

In this phase we will put everything together, making the app functional.

Open the App.js file and add the following code:

import React, { useState } from "react";

import { Dropdown } from "../components/Dropdown";
import { Editor } from "../components/Editor";
import { Highlighter } from "../components/Highlighter";

import * as themes from "react-syntax-highlighter/dist/esm/styles/hljs";
import * as languages from "react-syntax-highlighter/dist/esm/languages/hljs";

import "./App.css";

const defaultLanguage = `${"javascript" || Object.keys(languages).sort()[0]}`;
const defaultTheme = `${"atomOneDark" || Object.keys(themes).sort()[0]}`;

export default function App() {
  const [input, setInput] = useState("");
  const [language, setLanguage] = useState(defaultLanguage);
  const [theme, setTheme] = useState(defaultTheme);

  return (
    <div className="App">
      <div className="ControlsBox">
        <Dropdown
          defaultTheme={defaultLanguage}
          onChange={(e) => setLanguage(e.target.value)}
          data={languages}
        />
        <Dropdown
          defaultTheme={defaultTheme}
          onChange={(e) => setTheme(e.target.value)}
          data={themes}
        />
      </div>
      <div className="PanelsBox">
        <Editor
          placeHolder="Type your code here..."
          onChange={(e) => setInput(e.target.value)}
        />
        <Highlighter language={language} theme={themes[theme]}>
          {input}
        </Highlighter>
      </div>
    </div>
  );
}

First, we imported the Dropdown, Editor and Highlighter components as well as all the supported themes and languages from react-syntax-highlighter.

Then we set the defaultLanguage variable to javascript. If it is not available from the languages list we imported, we set the defaultlanguage to the first language available in the imported languages list. Same for the defaultTheme. We set defaultTheme variable to atomOneDark. If it is not available in the imported themes list, the defaultTheme value will be set to the first available theme from the imported themes list.

For the Dropdown components we set the defaultLanguage and defaultTheme that will be displayed once the app is first rendered. We also set the onChange behavior to update the language and theme variable states when the user makes selections from dropdowns. Finally, we passed in the data prop, that will generate the dropdown options list.

For the Editor component we set the placeHolder component to ask the user to enter some input once the app is first rendered as well as set onChange function that updates the input state variable each time the user write something in the Editor.

Finally, for the Highlighter component we passed in the language variable state, so it knows which language to render and the themes variable state so it knows how to style it.

The last thing left to do is to test our app. Check your terminal to see if the development server is still running (if it is not, run npm start) and open the browser.

You should be presented with the functional code editor and highlighter:

Demo

Conclusion

In this tutorial, we learned how to create a wireframe for an app, use states, create components, style them and create the app logic.

From now on, every time you need to pick up the most appropriate theme, you don't need to build a test application anymore. You will now have your own tool that you can use.

In the future, you can customize the project further by adding the auth system and database, so the user is able to save their snippets, creating a full-stack playground.

Thanks for reading and I hope you learned a thing or two from this tutorial.