contentEditable слишком большая боль для простых вещей, есть ещё техника, когда под прозрачный input просто подкладывается раскрашенный текст, примерно так:
<div class="input-container">
  <div class="input-shadow"></div>
  <input class="input"/>
</div>
<style>
.input-container{
  position: relative;
  overflow: hidden;
  display: inline-block;
}
.input {
  width: 10em;
  margin: 0;
  color: rgba(0, 0, 0, 0);
  caret-color: #000;
  background-color: transparent;
  position: relative;
  white-space: pre;
}
.input-shadow {
  position: absolute !important;
  outline: none !important;
  border-color: transparent;
  top: 0;
  left: 0;
  color: #999;
}
</style>
<script>
function init(input, shadow) {
  const style = getComputedStyle(input);
  const exclude = /\b(fill|stroke|color)\b/;
  
  Array.from(style).forEach(
    property => !exclude.test(property) && (shadow.style[property] = style[property])
  )
  const colors = [
    '#f00',
    "#000"
  ];
  function wrapText(text, index) {
    if(!text) return;
    const color = colors[index % (colors.length + 1) - 1];
    if(!color) 
      return document.createTextNode(text);
    const span = document.createElement('span');
    span.append(text);
    span.style.color = color;
    return span;
  }
  function onscroll() {
    shadow.style.left = -input.scrollLeft + 'px';
  }
  
  function oninput() {
    shadow.innerHTML = '';
    input
      .value
      .split(/(\d+)|([a-z]+)/i)
      .map(wrapText)
      .forEach(node => node && shadow.append(node))
  }
  input.addEventListener('scroll', onscroll);
  input.addEventListener('input', oninput);
  oninput();
  onscroll();
}
init(document.querySelector('.input'), document.querySelector('.input-shadow'));
</script>