Madars Bišs Madars Bišs (aka Madza) is a technical writer. In his spare time, he loves to explore new topics and contribute to open-source web development.

Building a React code editor and syntax highlighter from scratch

7 min read 2118

React Logo Over a Blue Background

Long gone are the days when developers coded in Notepad and blogs displayed the code blocks using just HTML. Highlighted code is much more pleasing to the eye and far 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 within the editor, meaning users will be able to switch between multiple languages and themes.

The source code will be available here, for reference.

Wireframing the React code editor and syntax highlighter

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

Wireframe

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

Inside 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 React code editor 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:

We made a custom demo for .
No really. Click here to check it out.

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. 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 rewrite 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 the index.js, which will render our app. Open it up and include the following code:

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

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

To render, we first imported the 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 of the DOM tree.

Now, open the index.css file and include the following styles:

css
* {
  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 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.

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

javascript
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.

We then created a App function, which 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.

Now, open the App.css file and add the following styles:

css
.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;
}

Here, 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 in React with useState hook

The data will change because, in our app, there will be user interaction triggered by the user selecting languages and themes. 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 the React ecosystem.

Open the App.js and add the following code:

javascript
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 included input, language, and theme variables inside the App function.

input will keep track of the input the user has written in the Editor, language will track the programming language the user has selected, and 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 to later import into 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 component

We will use the 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:

javascript
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>
  );
};

In this codeblock, 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 App to display the available theme options. We then sorted the options in alphabetical order.

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

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

css
.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.

Creating the Editor component

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:

javascript
import "./Editor.css";

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

Notice 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:

css
.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 the user has typed in.

Adding the react-syntax-highlighter package

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

npm i react-syntax-highlighter

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

javascript
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 Highlighter into App.js.

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

css
.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:

javascript
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 = <code>${"javascript" || Object.keys(languages).sort()[0]}<code>;
const defaultTheme = <code>${"atomOneDark" || Object.keys(themes).sort()[0]}<code>;

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>
  );
}

Let’s break down this code block.

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. The same is true for defaultTheme.

We also set the defaultTheme variable to atomOneDark. If it’s 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 defaultLanguage and defaultTheme, which will be displayed once the app is first rendered.

Notice that the onChange behavior will update the language and theme variable states when the user makes selections from dropdowns.

Finally, we passed in the data prop that generates 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. It also sets the onChange function that updates the input state variable each time the user writes something in the Editor.

Finally, for the Highlighter component, we passed in the language variable state — so it knows which language to render — as well as 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:

Final Code Editor Example

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 that users can save their snippets, creating a full-stack playground.

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

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Madars Bišs Madars Bišs (aka Madza) is a technical writer. In his spare time, he loves to explore new topics and contribute to open-source web development.

Leave a Reply