22<div style="height: 200px;">
<textarea placeholder="Enter HTML Source Code" id="editing" spellcheck="false" oninput="update(this.value); sync_scroll(this);" onscroll="sync_scroll(this);" onkeydown="check_tab(this, event);"></textarea>
<pre id="highlighting" aria-hidden="true">
<code class="language-html" id="highlighting-content">

&lt;span title=&quot;Start Typing&quot;&gt;Just start typing!&lt;/span&gt;
</code>
</pre>
</div>
<p>This is an example of a <b>syntax-highlighted <code>textarea</code></b>. The textarea, almost completely transparent except for the <code>caret-color</code>, is positioned on top of the syntax-highlighted block using <code>z-indexes</code>, and the result is synchronised whenever the user enters code.</p>
<p>I have covered this technique in <a href="https://css-tricks.com/creating-an-editable-textarea-that-supports-syntax-highlighted-code/">an article for CSS-Tricks</a>, and have further edited it to include suggestions from comments.</p>
<p><b>One comment suggested creating a custom element for editable syntax-highlighted code, and I thought that was a very good idea, so you can find the custom element demo I created <a href="https://codepen.io/WebCoder49/details/jOypJOx">here</a>.</b></p>
<p>Syntax Highlighting by <a href="https://prismjs.com/">Prism.js</a><br/>
Font from <a href="https://fonts.google.com/">Google Fonts</a><br/>
Made by <a href="https://codepen.io/WebCoder49/">WebCoder49</a><br/>
<b>Please feel free to use this technique as inspiration!</b></p>
/* Paragraphs */
* {
  font-family: "Fira Code", monospace;
}
p code {
  border-radius: 2px;
  background-color: #eee;
  color: #111;
}

/* Please see the article */

#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 highlighing tokens */
  font-size: 15pt;
  font-family: monospace;
  line-height: 20pt;
}


#editing, #highlighting {
  /* In the same place */
  position: absolute;
  top: 0;
  left: 0;
}


/* Move the textarea in front of the result */

#editing {
  z-index: 1;
}
#highlighting {
  z-index: 0;
}


/* Make textarea almost completely transparent */

#editing {
  color: transparent;
  background: transparent;
  caret-color: white; /* Or choose your favourite color */
}

/* Can be scrolled */
#editing, #highlighting {
  overflow: auto;
  white-space: nowrap; /* Allows textarea to scroll horizontally */
}

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

/* Syntax Highlighting from prism.js starts below, partly modified: */

/* PrismJS 1.23.0
https://prismjs.com/download.html#themes=prism-funky&languages=markup */
/**
 * prism.js Funky theme
 * Based on “Polyfilling the gaps” talk slides http://lea.verou.me/polyfilling-the-gaps/
 * @author Lea Verou
 */

code[class*="language-"],
pre[class*="language-"] {
	font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
	font-size: 1em;
	text-align: left;
	white-space: pre;
	word-spacing: normal;
	word-break: normal;
	word-wrap: normal;
	line-height: 1.5;

	-moz-tab-size: 4;
	-o-tab-size: 4;
	tab-size: 4;

	-webkit-hyphens: none;
	-moz-hyphens: none;
	-ms-hyphens: none;
	hyphens: none;
}

/* Code blocks */
pre[class*="language-"] {
	padding: .4em .8em;
	margin: .5em 0;
	overflow: auto;
	/* background: url('data:image/svg+xml;charset=utf-8,<svg%20version%3D"1.1"%20xmlns%3D"http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg"%20width%3D"100"%20height%3D"100"%20fill%3D"rgba(0%2C0%2C0%2C.2)">%0D%0A<polygon%20points%3D"0%2C50%2050%2C0%200%2C0"%20%2F>%0D%0A<polygon%20points%3D"0%2C100%2050%2C100%20100%2C50%20100%2C0"%20%2F>%0D%0A<%2Fsvg>');
	background-size: 1em 1em; - WebCoder49*/
  background: black; /* - WebCoder49 */
}

code[class*="language-"] {
	background: black;
	color: white;
	box-shadow: -.3em 0 0 .3em black, .3em 0 0 .3em black;
}

/* Inline code */
:not(pre) > code[class*="language-"] {
	padding: .2em;
	border-radius: .3em;
	box-shadow: none;
	white-space: normal;
}

.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
	color: #aaa;
}

.token.punctuation {
	color: #999;
}

.token.namespace {
	opacity: .7;
}

.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol {
	color: #0cf;
}

.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin {
	color: yellow;
}

.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.token.variable,
.token.inserted {
	color: yellowgreen;
}

.token.atrule,
.token.attr-value,
.token.keyword {
	color: deeppink;
}

.token.regex,
.token.important {
	color: orange;
}

.token.important,
.token.bold {
	font-weight: bold;
}
.token.italic {
	font-style: italic;
}

.token.entity {
	cursor: help;
}

.token.deleted {
	color: red;
}

/* Plugin styles: Diff Highlight */
pre.diff-highlight.diff-highlight > code .token.deleted:not(.prefix),
pre > code.diff-highlight.diff-highlight .token.deleted:not(.prefix) {
	background-color: rgba(255, 0, 0, .3);
	display: inline;
}

pre.diff-highlight.diff-highlight > code .token.inserted:not(.prefix),
pre > code.diff-highlight.diff-highlight .token.inserted:not(.prefix) {
	background-color: rgba(0, 255, 128, .3);
	display: inline;
}

/* End of prism.js syntax highlighting*/
function update(text) {
  let result_element = document.querySelector("#highlighting-content");
  // Update code
  result_element.innerHTML = text.replace(new RegExp("&", "g"), "&amp;").replace(new RegExp("<", "g"), "&lt;"); /* Global RegExp */
  // Syntax Highlight
  Prism.highlightElement(result_element);
}

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;
}

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; // after tab placed, where cursor moves to - 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;
  }
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/prism.min.js