How to Build Your Own Codepen-Style Editor App

Moshe Kerbel
Oct 22, 2020 · 14 min read

In this post, we are going to build together an online editor like

We will practice our JS and React skills and use some popular open-source libraries. Here is a live demo of what we are going to create.

So, let’s begin!

Creating a NextJS project

Since we would like our users to be able to save pens, we don’t want to create just a client-side project, but a full-stack one.

NextJS is an awesome framework that lets us create full-stack react applications with server-side rendering (SSR) and much more. Even though our use-case does not require SSR, I chose to use NextJS because it is super easy to create a full-stack app with it and deploy it later on.

Let’s create our project using the following command:

npx create-next-app my-code-pen# Or if you use yarnyarn create next-app my-code-pen

You should now be able to run a development server of the demo project that we just created:

cd my-code-pennpm run dev# Or if you use yarnyarn dev

You now have the server up and running, open it on the browser:


Layout — split the screen into resizable panels

We will begin the coding of our project by setting the basic design layout.

Our screen should be divided into 3 editors — HTML, CSS, JS, and 1 preview pane. Users should be able to resize these parts.

To get this behavior we need to implement some drag & drop ability. Luckily, there are many open-source React libraries that do just that.

We will use react-split-pane

npm install react-split-pane# Or if you use yarnyarn add react-split-pane

Now let’s start experimenting with the library.

In your project root, under pages you have the index.js, replace it with this code:

SplitPane can get 2 items and split them into a resizable pane — Vertically or Horizontally. If you look at the page in the browser you will see that it’s split into 2 equal divs — part1 & part2, but where is the resize functionality?

For this to work, we need to setup SplitPane with some css.

Let’s take the example from SplitPane docs:

But how can we apply this css to our page?

Well, since this CSS should apply to SplitPane it has to be imported as global. The way to achieve it with Next.js is with the following 2 files:

globals.css file — already exists under the styles directory in your root project. Replace its content with the above CSS code.

_app.js — is already configured in your app to inject the globals.css to every component in your app. You do not need to touch it.

Let’s look at the results in the browser, we are starting to get something basic that works:

Now when we have this working skeleton, let’s make it fit our needs and change Index.js code to the following:

As we said, SplitPane gets 2 items — so we have to nest a couple of it together. This is the result:

Let’s add some styling to make it look better:

First, we will change the borders to be wider and to have that same dark mode color #333642as in codepen

Let’s change globals.css to the following:

Definitely looks better:

Now what’s left is adding a background color and some font styling. But let's start splitting our code into components.

We will create 2 new files Editors.js Editors.module.css and place them under a new directory called components under our root directory.

Notice we are using css-modules, so all our CSS file names (except for global.css) must end with module.css , otherwise, you will get an error.

For now, as you can see, these files contain only basic CSS for the layout. We will modify them in a bit to actually become editors.

Now we can change index.js to use these new components instead of the early dummy divs:

And this is the outcome:


Currently, our editors are just placeholders with a title, let’s make them editable. We are going to use the open-source project react-ace

You can experiment with react-ace here — it lets you configure the editor with all the possible options: Language, theme, font size, tab size, and much more.

React-ace playground for experiment

Ok, so first thing, we will install react-ace packages:

npm install react-ace ace-builds# Or if you use yarnyarn add react-ace ace-builds

Now, let’s get back to our Editors.js and use react-ace.

We will start with very basic code, explain what’s going on, and then extend it:

Lines 2 — 6 are imports we are using for react-ace. We need to import the different styling for our editors —Javascript, HTML, and CSS. We also import the monokaitheme which gives us the nice dark mode styling that is used in Codepen.

Lines 26–30 is where we configure react-ace with the bare minimum which is required.

Let’s copy this code and go to the browser — start typing in the editors. We have editors for the different languages which highlight the keywords and give line numbers, but we still need to improve it a bit.

We want to enlarge the font size, extend the width to the maximum, add some margins, and set the tab size:

Our editors look pretty solid now, but what happens when we try to resize them?

Look at the height of the actual editors where we type in the text, it stays the same:

So what’s going on here?

We resized the editors but there is still a gap, the editors themselves did not get higher, they stayed with the same initial height. We want them to be responsive to the drag n drop event, so let's do it:

This is our index.js file with 2 new additions:

Line 7 — Hold the height value in the state, begin with the height of 485px.

Line 13 — We use the onDragFinished event of SplitPane to update the height value in the state. We remove 40px to compensate the editor's titles.

Now, we also need to update our Editors.js to use this height:

2 small additions were made:

Line 10–11 The JavascriptEditor — notice how we are now getting props and pass all of them to the Editor itself using the spread keyword — {...props}

We do it for the other 2 editors as well.

Line 22, Line 35 — We pass the height parameter to AceEditor

Now the editors are responsive to the height change:

Generating a preview page

Currently, we have 3 independent editors and a placeholder for the preview page. What we would like to do, is collect the inputs from all 3 editors, and use them to create a HTML document with css and javascript.

On each change of one of the editors, we need to regenerate the preview page. Let’s keep the input data of the editors in the state and that way we will respond to the state change and regenerate the preview page.

We are going to the editors.js page and adding the value and onChange props to the AceEditor :

Our index.js holds both the editors and the preview page placeholder, so we will manage the state of the editors from here. Here is the updated code — we will go over the changes we did:

Lines 11–13 — we are creating state variables to hold the state of each editor

Line 14–30 — we are adding an effect , so each time one of the editors' state value is changed, we set the output value. What is the output value? a template to generate an HTML document:

Line 43 - 44 — we pass the state value and setter to the HtmlEditor. We do that as well for the other 2 editors in lines 45 & 47

Line 58 — This is where the magic happens! We are creating an iframe that shows our preview page. Instead of giving it a path to a page in src , we are using the srcDocattribute which gets a string.

We are also using a class for the styling of the iframe, so let’s create an index.module.css file with the following:

Please note that if you are going to create some preview page like this in production, you should take care of possible vulnerability prevention. In such a case, you can read here about the sandbox attribute of iframe which can limit the execution of javascript code.

Using Debounce

As our code currently written, on every change in one of the editor's values, the preview page is regenerated. The problem is that these changes are too frequent — imagine that the user is typing this in the javascript editor:

alert(‘hello world’)

The preview page will be generated for each character change:

  1. a
  2. al
  3. ale
  4. aler
  5. alert

and so on…

It will produce undesired behavior, JS errors in this specific case, and also performance issues.

In order to overcome this problem, we will use the debounce technique.

The idea is to force a function to wait a certain amount of time before running again. Instead of calling setHtmlValue directly, we will call a wrapper debounceHtml which will execute setHtmlValue at least 300 milliseconds after its previous execution.

Here you can learn more about debounce

Under your root project create a directory called utils and there create file called useDebounce.js

We will create the following useDebounce hook and update our index.js to use it.

Saving and loading pens

Our app should be able to save and load pens.

We will write 3 API functions:

  1. Load an existing pen
  2. Save a new pen
  3. Update an existing pen

All APIs will interact with a database, but if you are not interested in creating a database, I am going to show a way to complete this guide with a full working app, by using a fake in-memory database.

To make things easier, we will write our APIs step by step, first as dummy APIs with static data, in order to create an end to end relationship with our editor, and only then, we will rewrite them to use a database.

Load pen API

Our API will get as an input, an id of a pen to retrieve and as a result will give us back the pen’s data.

http://localhost:3000/api/pens/123 — will give us back the data for pen with id 123

http://localhost:3000/api/pens/456 — will give us back the data for pen with id 456

We will create a file called [id].js and place it inside a new pens directory inside thepages directory.

pages / api / pens / [id].js

You are not mistaken, [id].js is the name we want to give to the file. By doing it, NextJS knows that it needs to execute this file anytime there is a request to …/api/pens/[id] — where id can be anything — for example — /api/pens/blabla.

So for now, we want to add a GET method to our file. This method will retrieve a dummy pen which says “hello from pen [id]” with blue text color and a js console.log message:

We can access our api directly from the browser:


Response for api

Let’s integrate our editor with the new api in our index.js.

When our index page is mounting, we would like to fetch the required pen, using the id from the querystring. We will add the following effect that takes the result of the api and set the editors with it:

Check out the result in the browser:


Did you notice what is missing here?

A loading indication

When we use fetch to request data, it can take time. Currently, during that time the editors stay empty. We would like to change it and indicate to the user that the pen’s data is being loaded.

We are going to add the following:

  1. Create a stateful variable called loading and set it to true.
  2. Set loading to false when the load API request is finished.
  3. Change what we render — instead of always rendering the editors, check first — if loading=true then render a loading indication, otherwise, render the editors.
  4. Add a css class for the loading indication. Cover the whole screen with loading... text in the center of the page.

Here is the updated code of index.js and index.module.css :

If you would like to see how the loading indication looks, simply delay the response of the API in [id].js by adding this before setting the json result:

Save a new pen API

API should get as an input the data to save, add it to the database and return the id of the new record in the database.

Let’s do it step by step, as we did for the load api, write something dummy first, integrate with the editors, and only afterward, write the actual database code.

We will add a PUT case to our [id].js file that simply returns the id of the newly saved record:

Update an existing pen API

Notice how in line 13 we also added a POST case. This one will handle the update of an existing pen. In such case, we already have an id, so our results would be an indication that the record was updated correctly: updatedRecord: true

Integrate the new APIs with the editors

We would like our index.js to let us create a new pen and update a pen we are modifying.

Let’s start with the UI — we will add 2 buttons on the top, left side of the page, it will look like this:

Inside index.js we will add this before our SplitPane editors:

Because our mainSplitPane component, by default starts from the position top: 0, we need to add it a margin, otherwise, we won’t be able to see our 2 new buttons:

We also used 2 new classes header and button , so let’s add them to our index.module.css :

Now when we have our buttons, let’s implement a save function to call it on the save button click.

Our function will do the following:

  1. Change the text of the button from Save to Saving , using an isSaving state variable.
  2. Setup a fetch request
  3. If we have an id already then this is an exiting pen, we would like to update it, so set method to be post
  4. If we don’t have an id then this is a new pen, we need to create it — so we set method to put

5. After the request is finished, change again the isSaving state variable to false

6. Check the result of our fetch, if we get back updatedRecord=false , it means that this is a new request and we got a new id back from the server. We need to change the URL of the page, so we will use Router.Push

We will add it to our index.js :

Don’t forget to set the onClick of the Save button, and modify it’s text according to isSaving:

Here is the complete code for both index.js and index.modules.css:

Last Part — Connect our app to a database

Last thing we need to do to finish our app is connect it to a database. We will do the following:

  1. Setup a mongodb database or create a fake mocked mongodb.
  2. Create a connect function to connect to our database from nodejs.
  3. Update our 3 apis to load and save data using the database.

Step 1 — database creation:

MongoDB Atlas is a global cloud database service which lets you create a free mongodb database.

Creating a database requires you to create an account in Atlas and follow a few steps where in the end, you will get a connection string you will use in your app:


You can use this short video tutorial that explains how to do it step by step.

After the setup please create a database collection called — pens

We will use it later on.

If you are not interested in creating a database, in the end of the next step I am going to show a way how to connect to a faked, mocked, mongodb database.

Step 2 — connect to the database

Let’s install the mongodb package:

npm install mongodb# Or if you use yarnyarn add mongodb

We will import the MongoClient and use it to create a new client. We will supply the connection string of our database, connect to it and return an instance of our db.

Under the root directory of the project, create a new directory called utils and there create a new file calleddatabase.js with the above code.

Use a fake database

mongo-mock is an in-memory ‘pretend’ mongodb. It gives you an interface which is compatible with the real mongo-db module. This way you can easily switch between the two.

Let’s install the mongo-mock package:

npm install mongo-mock# Or if you use yarnyarn add mongo-mock

Under the root directory of the project, create a new directory called utils and there create a new file calledfake-database.js with the following code:

import { MongoClient, ObjectId } from "mongo-mock";

async function connect() {
const db = await MongoClient.connect("mongodb://mocked-database");
const collection = await db.createCollection("pens");

return {db , collection};

export { connect, ObjectId };

Step 3 — update our APIs to use the database

We will go to [id].js and import the connect function and ObjectId from our relvant implementation, either the real or fake db:

Load API:

Should do the following:

  1. Connect to the database
  2. Inside the pens collection, find the record with ObjectId that equals the id quey param
  3. Return a valid 200 response and set the response to the data of the record we found in the db
  4. If no data was found return a 404 result.
  5. In case of an error return a 500 result.

We will replace the GET case with this new code:

Save API:

Should do the following:

  1. Get the params from the body of the request: html css js
  2. Connect to the database
  3. Inside the pens collection, insert a new record object with the html css js data
  4. Return a valid 200 response with the newly inserted id as the data.

We will replace the PUT case with this new code:

Update API:

Instead of insertOne as the save API does, we will use updateOne and look for the id of the existing pen that we got as a parameter in the body of the request.

Once done, we will return as data, an indication of updatedRecord: true.

We will replace the POST case with this new code:

Here is the complete code for [id].js:

That’s it, we are done, our app is finally complete now!

The full working code can be found here.

Feel free to leave a comment below with any questions or feedback you might have.

Frontend Weekly

It's really hard to keep up with all the front-end…

Frontend Weekly

It's really hard to keep up with all the front-end development news out there. Let us help you. We hand-pick interesting articles related to front-end development. You can also subscribe to our weekly newsletter at

Moshe Kerbel

Written by

Frontend developer

Frontend Weekly

It's really hard to keep up with all the front-end development news out there. Let us help you. We hand-pick interesting articles related to front-end development. You can also subscribe to our weekly newsletter at

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store