How to Build Your Own Codepen-Style Editor App

In this post, we are going to build together an online editor like Codepen.com
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 #333642
as 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:

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

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 monokai
theme 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 srcDoc
attribute 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:
a
al
ale
aler
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:
- Load an existing pen
- Save a new pen
- 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:
http://localhost:3000/api/pens/123456

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:
http://localhost:3000/?id=1234

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:
- Create a stateful variable called
loading
and set it totrue
. - Set
loading
tofalse
when the load API request is finished. - Change what we render — instead of always rendering the editors, check first — if
loading=true
then render a loading indication, otherwise, render the editors. - 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:
- Change the text of the button from
Save
toSaving
, using anisSaving
state variable. - Setup a fetch request
- If we have an
id
already then this is an exiting pen, we would like to update it, so setmethod
to bepost
- If we don’t have an
id
then this is a new pen, we need to create it — so we setmethod
toput
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:
- Setup a mongodb database or create a fake mocked mongodb.
- Create a connect function to connect to our database from nodejs.
- 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:
`mongodb+srv://your-username:your-password@cluster0.ldrfm.mongodb.net/your-db-name?retryWrites=true&w=majority`;
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:
- Connect to the database
- Inside the
pens
collection, find the record withObjectId
that equals theid
quey param - Return a valid
200
response and set the response to thedata
of the record we found in the db - If no data was found return a
404
result. - 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:
- Get the params from the body of the request:
html
css
js
- Connect to the database
- Inside the
pens
collection, insert a new record object with thehtml css js
data - 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.