Skip to main content Accessibility Feedback

How to create your own CodePen clone with vanilla JavaScript

I’m working on a new project-based JavaScript course for beginners and aspiring developers.

In addition to a ton of projects, I wanted to include little mini-challenges with each lesson. And to do that, I want to include a code sandbox (like CodePen) right in the tutorial.

Rather than loading buckets of React like most libraries for this require, I decided to build my own. Let’s look at how!

An example challenge

For example, imagine that you’ve just learned about the Array.prototype.filter() method. Then, you’re presented with this challenge (newsletter subscribers, click through to view this part).

Anything you type in the code sandbox above automatically runs in its own isolated container, and anything you log to the console is displayed in the UI.

In this example, I’ve made the console very prominent since it’s a JavaScript-heavy exercise, but it can also be configured to display a rendered UI with HTML and CSS.

So… how do build something like this?

Like so many things code-related, the basic implementation was shockingly simple, and the details were annoyingly complicated.

Today, I’m going to look at how to get a basic sandbox environment setup. In a future article, if you’re interested, I can talk through how I got it setup with syntax highlighting and a nicer UI.

Let’s dig in!

Creating the input elements

To start, you add a label and textarea for each type of input you’re supporting. In our case, let’s create ones for html, css, and js.

<label for="html">HTML</label>
<textarea id="html"></textarea>

<label for="css">CSS</label>
<textarea id="css"></textarea>

<label for="js">JavaScript</label>
<textarea id="js"></textarea>

When typing code in these, a lot of browsers will try to autocorrect words or capitalize things for you automatically. We don’t want.

Let’s disable autocorrect, autocapitalize, and translate on each of our fields.

<label for="html">HTML</label>
<textarea id="html" spellcheck="false" autocorrect="off" autocapitalize="off" translate="no"></textarea>

To keep our code encapsulated and protect ourselves from cross-site scripting attacks, we want to render whatever the user types into an iframe. Let’s create that now, too.

<p><strong>Result</strong></p>
<iframe id="result"></iframe>

Now, we have a place for users to type code, and a place to render and run it.

Rendering code into the sandbox

First, let’s get all four of our elements using the document.querySelector() method.

// Get elements
let html = document.querySelector('#html');
let css = document.querySelector('#css');
let js = document.querySelector('#js');
let result = document.querySelector('#result');

Next, we’ll use event delegation to listen for input events in our document, and run an inputHandler() method when they happen.

// Listen for input events
document.addEventListener('input', inputHandler);

In the inputHandler() method, we’ll check if the event.target, the field that was updated, was the html, css, or js field.

If it’s none of them, we’ll return early to end the function. Otherwise, we’ll run an updateIframe() function that will actually render and run the code for us.

/**
 * Handle input events on our fields
 * @param  {Event}  event The event object
 */
function inputHandler (event) {

	// Only run on our three fields
	if (
		event.target !== html &&
		event.target !== css &&
		event.target !== js
	) return;

	// Render code into the sandbox
	updateIframe();

}

Normally, you can’t modify the code in an iframe. But… because our iframe is hosted from the same domain as the site it’s being loaded and modified on, we can!

The contentWindow.document property gives us access to the actual DOM inside our iframe element.

We’ll use the iFrame.contentWindow.document.open() method to open it up, and the Element.writeln() method to inject HTML into the iframe.

In our case, we want to render the value’s of the html, css, and js fields. We’ll wrap the css and js in style and script elements, respectively. Then, we’ll use the iFrame.contentWindow.document.close() method to close the iframe back up.

/**
 * Update the iframe
 */
function updateIframe () {
	result.contentWindow.document.open();
	result.contentWindow.document.writeln(
		`${html.value}
		<style>${css.value}</style>
		<script type="module">${js.value}<\/script>`
	);
	result.contentWindow.document.close();
}

Now, whenever the user types, there code is rendered into the result element in real time!

A quick performance fix

As you may have already guessed, constantly updating and repainting the iframe in real time can be a performance pit.

Let’s add a 500 millisecond delay (half a second) from when the user stops typing to when we actually render, using a technique called debouncing.

First, we’ll add a debounce variable that will track the current render set to happen.

// Store debounce timer
let debounce;

Inside the inputHandler(), we’ll pass our updateIframe() function into the setTimeout() method, with a 500 millisecond delay. We’ll assign the returned timeout ID to the debounce variable.

Before setting up our function to run, we’ll pass the debounce variable (and any existing scheduled render) into the clearTimeout() method, which will cancel it.

Now, the UI won’t render until half a second after the user stops typing.

/**
 * Handle input events on our fields
 * @param  {Event}  event The event object
 */
function inputHandler (event) {

	// Only run on our three fields
	if (
		event.target !== html &&
		event.target !== css &&
		event.target !== js
	) return;

	// Debounce the rendering for performance reasons
	clearTimeout(debounce);

	// Set update to happen when typing stops
	debounce = setTimeout(updateIframe, 500);

}

Another quirk: JavaScript bugs

One other little gotcha is around JavaScript.

Let’s say you typed this into the #js field…

let wizards = ['Merlin', 'Ursula', 'Gandalf'];

Then, a few seconds later, you added some more code…

let wizards = ['Merlin', 'Ursula', 'Gandalf'];
console.log(wizards);

Because we’re rendering the entire contents of the textarea into the iframe, the browser rendering engine in the iframe thinks you’re trying to redeclare the wizards variable, and will throw an error.

To get around that, we’ll clone our iframe, replace it with the new one, and render into that, giving us a “fresh start” each time.

/**
 * Update the iframe
 */
function updateIframe () {

	// Create new iframe
	let clone = result.cloneNode();
	result.replaceWith(clone);
	result = clone;

	// Render
	result.contentWindow.document.open();
	result.contentWindow.document.writeln(
		`${html.value}
		<style>${css.value}</style>
		<script type="module">${js.value}<\/script>`
	);
	result.contentWindow.document.close();

}

This is the one part of this whole thing that feels a little janky to me, but it’s far easier and more resilient than trying to diff and modify JS outputs before rendering them.

Play with this code!

Want to try it yourself? Download the source code here and start hacking away at it.

If you have any questions or want me to dig into how I added syntax highlighting, let me know!