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>