Skip to main content Accessibility Feedback

How to add syntax highlighting to code as a user types in realtime with vanilla JavaScript

On Monday, I shared a new interactive Code Sandbox I had built using vanilla JavaScript, and explained how I live render code into an iframe.

Today, I wanted to share how I syntax highlight code as the user types in real time.

Let’s dig in!

The one simple trick that makes this work!

I tried a bunch of things to get this to work, but the trick that made it happen ended up being stupidly simple.

  1. I added a pre element (with nested code element) in the UI before my each textarea.
  2. I position the textarea directly on top of the pre element using CSS Grid.
  3. I set the text color and background on my textarea to transparent, so you can see the pre element behind it.
  4. I otherwise style them to look the same: same typeface, font size, padding, etc.
  5. When the user types, I mirror their text into the pre > code element in realtime, and syntax highlight that.

The result: when users type, their text gets mirrored and highlighted into the pre element, but because the actual text they type is transparent, it looks like the text they type is what’s being highlighted.

Let’s look at how that actual works.

Adding pre and code elements

I updated the UI to include nested pre and code elements. Then I wrapped both those and the textarea in a div with the .editor class.

<div class="editor">
	<pre class="lang-html"><code></code></pre>
	<textarea id="html" spellcheck="false" autocorrect="off" autocapitalize="off" translate="no"></textarea>
</div>

Next, I added some CSS to turn the .editor into a grid layout.

I positioned the pre and textarea elements in the example same spot. Because the textarea is second in the layout, it sits on top.

.editor {
	display: grid;
	grid-template-columns: 1fr;
	grid-template-rows: 1fr;
	gap: 0;
}

.editor pre,
.editor textarea {
	grid-area: 1 / 1 / 2 / 2;
}

Styling the textarea, pre, and code elements

This part is largely a matter of taste. I added CSS to make my pre and code elements look the way I want, and then added .editor textarea to those code declarations so that it would look the same.

pre,
.editor textarea {
	background-color: #f7f7f7;
	color: #272727;
	display: block;
	line-height: 1.5;
	margin: 0 0 1.5625em;
	overflow: auto;
	padding: 0.8125em;
	white-space: pre-wrap;
	word-break: break-all;
}

code {
	word-wrap: break-word;
}

pre,
code,
.editor textarea {
	font-family: Menlo, Monaco, "Courier New", monospace;
	font-size: 0.875em;
}

pre code {
	color: inherit;
	font-size: 1em;
}

I added one additional style block just for the .editor textarea element.

This sets the background-color and color to transparent, and removes some of the default textarea styling. It also adds a caret-color property. This makes the caret visible as the user types, even though the text itself is invisible.

.editor textarea {
	background-color: transparent;
	border: none;
	color: transparent;
	caret-color: #000;
	overflow: hidden;
	resize: none;
	width: 100%;
}

Mirroring the text

Inside the inputHandler() function, before setting up my debounce function to render the iframe, I mirror the textarea text into the pre > code element.

Because I have tight control over the layout on this, I can use the Element.previousElementSibling element to get the pre element, and the firstChild property of that to get the nested code element.

I use the Node.textContent property to set its text to whatever the value of the textarea (or event.target) is.

/**
 * 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;

	// Clone text into pre immediately
	let code = event.target.previousElementSibling.firstChild;
	if (!code) return;
	code.textContent = event.target.value;

	// ...

}

Syntax highlighting the code

This is actually the most code-heavy part of the whole library, because it requires a fair bit of JS to do properly.

I used the fantastic Prism.js library from Lea Verou. After you select your desired theme, download the development version (at least of the CSS).

By default, Prism.js will highlight every pre element with a .lang- or .language- class on the page, but we want to control when and where it runs. To do that, we can set it to manual mode.

You can do that by either adding a [manual] attribute to the script element when you load it, or setting it with some JS configurations. Here, we’ll use the attribute.

<script src="prism.js" manual></script>

In the Prism.js CSS file, we can remove all of the default pre and code styling. We only need the syntax related stuff: the items prefixed with .token in the CSS file.

We’ll load that on our page as well.

<link rel="stylesheet" type="text/css" href="prism.css">

Back in our inputHandler() function, we’ll run the Prism.highlightElement() method to highlight the code in the element we just mirrored our text into.

// Clone text into pre immediately
let code = event.target.previousElementSibling.firstChild;
if (!code) return;
code.textContent = event.target.value;

// Highlight the syntax
Prism.highlightElement(code);

Now, as the user types, their code is mirrored and highlighted.

Try it out!

You can download the code from this tutorial on GitHub.

For me, the next step after this was to create a Web Component: code-sandbox. This lets me easily setup multiple sandboxes on a page, only include the code types I need, and write just the minimal amount of HTML required.

The Web Component handles rendering the pre and code elements, injecting the required styles, wiring up event listeners, and more. But… that’s a much bigger tutorial.