Creating an Editable Textarea That Supports Syntax-Highlighted Code

When I was working on a project that needed an editor component for source code, I really wanted a way to have that editor highlight the syntax its typed. There are projects like this, like CodeMirror, Ace, and Monaco, but they are all heavy-weight, full-featured editors, not just editable textareas with syntax highlighting like I wanted.

It took a little finagling, but I wound up making something that does the job and wanted to share how I did it, because it involves integrating a popular syntax highlighting library with HTML’s editing capabilities, as well as a few interesting edge cases to take into consideration.

Go ahead and give it a spin as we dig in!

The problem

First, I tried using the contenteditable attribute on a div. I typed some source code into the div and ran it through Prism.js, a popular syntax highlighter, on onkeyup via JavaScript. Seems like a decent idea, right? We have an element that can be edited on the front end, and Prism.js applies its syntax styling to what’s typed in the element.

Chris covers how to use Prism.js in this video.

But that was a no-go. Each time the content in the element changes, the DOM is manipulated and the user’s cursor is pushed back to the start of the code, meaning the source code appears backwards, with the last characters at the start, and the first characters at the end.

White monospaced text on a black background that does not spell a coherent word.
Backwards code: not very useful

Next, I tried about using a <textarea> but that also didn’t work, as textareas can only contain plain text. In other words, we’re unable to style the content that’s entered. A textarea seems to be the only way to edit the text without unwanted bugs — it just doesn’t let Prism.js do its thing.

Prism.js works a lot better when the source code is wrapped in a typical <pre><code> tag combo — it’s only missing the editable part of the equation.

So, neither seems to work by themselves. But, I thought, why not both?

The solution

I added both a syntax-highlighted <pre><code> and a textarea to the page, and made the innerText content of <pre><code> change onkeyup, using a JavaScript function. I also added an aria-hidden attribute to the <pre><code> result so that screen readers would only read what is entered into the <textarea> instead of being read aloud twice.

<textarea id="editing" onkeyup="update(this.value);"></textarea>

<pre id="highlighting" aria-hidden="true">
  <code class="language-html" id="highlighting-content"></code>
</pre>
function update(text) {
  let result_element = document.querySelector("#highlighting-content");
  // Update code
  result_element.innerText = text;
  // Syntax Highlight
  Prism.highlightElement(result_element);
}
The HTML for a link element pointed to CSS-Tricks is in black monospace on a white background above the same HTML link markup, but syntax-highlighted on a black background.
Now it’s editable and highlighted!

Now, when the textarea is edited — as in, a pressed key on the keyboard comes back up — the syntax-highlighted code changes. There are a few bugs we’ll get to, but I want to focus first on making it look like you are directly editing the syntax-highlighted element, rather than a separate textarea.

Making it “feel” like a code editor

The idea is to visibly merge the elements together so it looks like we’re interacting with one element when there are actually two elements at work. We can add some CSS that basically allows the <textarea> and the <pre><code> elements to be sized and spaced consistently.

#editing, #highlighting {
  /* Both elements need the same text and space styling so they are directly on top of each other */
  margin: 10px;
  padding: 10px;
  border: 0;
  width: calc(100% - 32px);
  height: 150px;
}

#editing, #highlighting, #highlighting * {
  /* Also add text styles to highlighting tokens */
  font-size: 15pt;
  font-family: monospace;
  line-height: 20pt;
}

Then we want to position them right on top of each other:

#editing, #highlighting {
  position: absolute;
  top: 0;
  left: 0;
}

From there, z-index allows the textarea to stack in front the highlighted result:

/* Move the textarea in front of the result */
#editing {
  z-index: 1;
}

#highlighting {
  z-index: 0;
}

If we stop here, we’ll see our first bug. It doesn’t look like Prism.js is highlighting the syntax, but that is only because the textarea is covering up the result.

Where has the highlighting gone? (Clue: It’s hiding in the background.)

We can fix this with CSS! We’ll make the <textarea> completely transparent except the caret (cursor):

/* Make textarea almost completely transparent */
#editing {
  color: transparent;
  background: transparent;
  caret-color: white; /* Or choose your favorite color */
}

Ah, much better!

HTML markup for a link element pointed to CSS tricks in a syntax-highlighted monospace font on a black background.
Where will this link take you? You decide.

More fixing!

Once I got this far, I played around with the editor a bit and was able to find a few more things that needed fixing. The good thing is that all of the issues are quite easy to fix using JavaScript, CSS, or even HTML.

Remove native spell checking

We’re making a code editor, and code has lots of words and attributes that a browser’s native spell checker will think are misspellings.

Showing the basic HTML markup for a link with syntax highlighting on a black background, where the href attribute is underlined in red.

Spell checking isn’t a bad thing; it’s just unhelpful in this situation. Is something marked incorrect because it is incorrectly spelled or because the code is invalid? It’s tough to tell. To fix this, all we need is to set the spellcheck attribute on the <textarea> to false:

<textarea id="editing" spellcheck="false" ...>

Handling new lines

Turns out that innerText doesn’t support newlines (\n).

White monospace text on a black background. The first line says "Hello, World" and the second line says "World" and is highlight in blue.
Even though “World!” should be on a new line, the syntax highlighted section displays it on the same line.

The update function needs to be edited. Instead of using innerText, we can replace the open bracket character (<) with &lt;. This prevents new HTML tags from being created, allowing the actual source code displays instead of the browser attempting to render the code.

result_element.innerHTML = text.replace(new RegExp("<", "g"), "<"); /* Global RegExp */
Showing the basic HTML document boilerplate with syntax highlighting on a black background. The body element contains the markup yo a paragraph that contains a link that says "CSS-Tricks is brilliant!"

Scrolling and resizing

Here’s another thing: the highlighted code cannot scroll while the editing is taking place. And when the textarea is scrolled, the highlighted code does not scroll with it.

First, let’s make sure that both the textarea and result support scrolling:

/* Can be scrolled */
#editing, #highlighting {
  overflow: auto;
}

Then, to make sure that the result scrolls with the textarea, we’ll update the HTML and JavaScript like this:

<textarea id="editing" onkeyup="update(this.value); sync_scroll(this);" onscroll="sync_scroll(this);"></textarea>
function sync_scroll(element) {
  /* Scroll result to scroll coords of event - sync with textarea */
  let result_element = document.querySelector("#highlighting");
  // Get and set x and y
  result_element.scrollTop = element.scrollTop;
  result_element.scrollLeft = element.scrollLeft;
}

Some browsers also allow a textarea to be resized, but this means that the textarea and result could become different sizes. Can CSS fix this? Of course it can. We’ll simply disable resizing:

/* No resize on textarea */
#editing {
  resize: none;
}

Indenting lines

One of the trickier things to adjust is how to handle line indentations in the result. The way the editor is currently set up, indenting lines with spaces works fine. But, if you’re more into tabs than spaces, you may have noticed that those aren’t working as expected.

JavaScript can be used to make the Tab key properly work. I have added comments to make it clear what is happening in the function.

<textarea ... onkeydown="check_tab(this, event);"></textarea>
function check_tab(element, event) {
  let code = element.value;
  if(event.key == "Tab") {
    /* Tab key pressed */
    event.preventDefault(); // stop normal
    let before_tab = code.slice(0, element.selectionStart); // text before tab
    let after_tab = code.slice(element.selectionEnd, element.value.length); // text after tab
    let cursor_pos = element.selectionEnd + 2; // where cursor moves after tab - 2 for 2 spaces
    element.value = before_tab + "  " + after_tab; // add tab char - 2 spaces
    // move cursor
    element.selectionStart = cursor_pos;
    element.selectionEnd = cursor_pos;
  }
}

The final result

Not too crazy, right? All we have are <textarea>, <pre> and <code> elements in the HTML, a new lines of CSS that stack them together, and a syntax highlighting library to format what’s entered. And what I like best about this is that we’re working with normal, semantic HTML elements, leveraging native attributes to get the behavior we want, leaning on CSS to create the illusion that we’re only interacting with one element, then reaching for JavaScript to solve some edge cases.

While I used Prism.js for syntax highlighting, this technique will work with others. It would even work with a syntax highlighter you create yourself, if you want it to. I hope this becomes useful, and can be used in many places, whether it’s a WYSIWYG editor for a CMS, or even a forms where the ability to enter source code is a requirement like a front-end job application or perhaps a quiz. It is a<textarea>, after all, so it’s capable of being used in any form — you can even add a placeholder if you need to!


The post Creating an Editable Textarea That Supports Syntax-Highlighted Code appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

The Cleanest Trick for Autogrowing Textareas

Earlier this year I wrote a bit about autogrowing textareas and inputs. The idea was to make a <textarea> more like a <div> so it expands in height as much as it needs to in order to contain the current value. It’s almost weird there isn’t a simple native solution for this, isn’t it? Looking back at that article, none of my ideas were particularly good. But Stephen Shaw’s idea that I linked to toward the end of it is actually a very good idea for this, so I wanted to shine some light on that and talk through how it works, because it seems like it’s the final answer to how this UX can be done until we get something native and better.

Here’s the demo in case you just want a working example:

The trick is that you exactly replicate the content of the <textarea> in an element that can auto expand height, and match its sizing.

So you’ve got a <textarea>, which cannot auto expand height.

Instead, you exactly replicate the look, content, and position of the element in another element. You hide the replica visually (might as well leave the one that’s technically-functional visible).

Now all three elements are tied to each other. Whichever of the children is tallest is will push the parent to that height, and the other child will follow. This means that the minimum height of the <textarea> will become the “base” height, but if the replicated text element happens to grow taller, everything will grow taller with it.

So clever. I love it so much.

You need to make sure the replicated element is exactly the same

Same font, same padding, same margin, same border… everything. It’s an identical copy, just visually hidden with visibility: hidden;. If it’s not exactly the same, everything won’t grow together exactly right.

We also need white-space: pre-wrap; on the replicated text because that is how textareas behave.

This is the weirdest part

In my demo, I’m using ::after for the replicated text. I’m not sure if that’s the best possible approach or not. It feels clean to me, but I wonder if using a <div aria-hidden="true"> is safer for screen readers? Or maybe the visibility: hidden; is enough for that? Anyway, that’s not the weird part. This is the weird part:

content: attr(data-replicated-value) " ";

Because I am using a pseudo-element, that’s the line that takes the data attribute off the element and renders the content to the page with that extra space (that’s the weird part). If you don’t do that, the end result feels “jumpy.” I can’t say I entirely understand it, but it seems like it respects the line break behavior across the textarea and text elements better.

If you don’t want to use a pseudo-element, hey, fine with me, just watch for the jumpy behavior.


Special high fives to Will Earp and Martin Tillmann who both randomly emailed on the same exact day to remind me how clever Shaw’s technique is. Here’s an example Martin made with Alpine.js and Tailwind that also ends up kinda like a one-liner (but note how it’s got the jumpy thing going on).

I’m sure ya’ll could imagine how to do this with Vue and React and whatnot in a way that can very easily maintain state across a textarea and another element. I’m not going to include examples here, partially because I’m lazy, but mostly because I think you should understand how this works. It will make you smarter and understand your site better.


The post The Cleanest Trick for Autogrowing Textareas appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

“resize: none;” on textareas is bad UX

Catalin Rosu:

Sometimes you need to type a long reply that consists of many paragraphs and wrapping that text within a tiny textarea box makes it hard to understand and to follow as you type. There were many times when I had to write that text within Notepad++ for example and then just paste the whole reply in that small textarea. I admit I also opened the DevTools to override the resize: none declaration but that’s not really a productive way to do things.

Removing the default resizeability of a <textarea> is generally user-hurting vanity. Even if the resized textarea "breaks" the site layout, too bad, the user is trying to do something very important on this site right now and you should not let anything get in the way of that. I know the web is a big place though, so feel free to prove me wrong in the comments.

This must have been cathartic for Catalin, who has been steadily gaining a reputation on Stack Overflow for an answer on how to prevent a textarea from resizing from almost a decade ago.

Direct Link to ArticlePermalink

The post “resize: none;” on textareas is bad UX appeared first on CSS-Tricks.

Vue Tutorial 5 — Form Data Binding

Granny now wants us to help her make a list of all the guests attending the party. She could use pen and paper, but her grandson is a programmer and she wants a Vue app. 

In this tutorial, we’ll accept user input (information about a guest) and display it in the list, similar to when we displayed the list of groceries. However, today we’ll use the  v-model directive to create two-way data bindings on the form input.