
本文介绍如何实时高亮 textarea 编辑器左侧行号栏中与光标所在行对应的数字,通过监听光标位置、精准计算行号,并动态更新 css 样式实现视觉反馈。
本文介绍如何实时高亮 textarea 编辑器左侧行号栏中与光标所在行对应的数字,通过监听光标位置、精准计算行号,并动态更新 css 样式实现视觉反馈。
在构建类代码编辑器的 UI 时,一个关键的用户体验细节是:当用户在
- 仅响应 click,不响应键盘导航(方向键、Home/End 等);
- 高亮状态不会自动清除,导致多行残留背景色;
- 默认为 inline 元素,background-color 无法完整填充行高,需显式设置 display: block 或 inline-block 并确保高度对齐。
以下是一个健壮、可维护的解决方案:
✅ 正确实现要点
- 统一监听所有光标变更事件:使用 input、click、keydown(含方向键)、focus 四类事件组合,覆盖全部交互场景;
- 每次高亮前先清除旧高亮:避免残留;
- 确保行号 可正确渲染背景:设置 display: inline-block + 显式 height 和 line-height 对齐;
- 行号计算逻辑优化:基于 selectionStart 和换行符 \n 的累计偏移量,返回从 1 开始的行号(更符合直觉)。
✅ 完整可运行代码示例
<style>
body, textarea {
font-family: Consolas, "Courier New", Courier, monospace;
}
.editor {
display: inline-grid;
grid-template-columns: 3em auto;
gap: 10px;
line-height: 21px;
background: rgb(40 42 58);
border-radius: 2px;
overflow-y: auto;
max-height: 243px;
}
.editor > * {
padding-top: 10px;
padding-bottom: 10px;
}
.numbers {
text-align: right;
background: #333;
padding-right: 5px;
}
.numbers span {
counter-increment: linenumber;
display: inline-block; /* ✅ 关键:使 background 生效 */
height: 21px; /* ✅ 与 line-height 一致 */
line-height: 21px;
padding: 0 4px;
}
.numbers span::before {
content: counter(linenumber);
color: #888;
}
.numbers span.active {
background-color: #ffcc00;
color: #000;
font-weight: bold;
}
textarea {
line-height: 21px;
border: 0;
background: transparent;
color: #fff;
min-width: 500px;
outline: none;
resize: none;
padding-right: 10px;
}
</style><div class="editor">
<div class="numbers"></div>
<textarea id="textarea" rows="10" cols="50" spellcheck="false"></textarea>
</div>
<script>
const ta = document.getElementById('textarea');
const numbersDiv = document.querySelector('.numbers');
// 初始化内容
ta.value = `I love
Tanzania
Nasila
America
Football
JavaScript
Chess
Ang Probinsyano
Startimes
China`;
// 生成行号 DOM
function updateRowNumbering() {
const lines = ta.value.split('\n').length;
numbersDiv.innerHTML = Array(lines).fill('<span>').join('');
}
// 获取当前光标所在行号(从 1 开始)
function getCurrentLineNumber() {
const pos = ta.selectionStart;
const text = ta.value;
let line = 1;
for (let i = 0; i < pos && i < text.length; i++) {
if (text[i] === '\n') line++;
}
return line;
}
// 高亮指定行号,自动清除上一个高亮
function highlightLineNumber(lineNumber) {
// 清除所有 active 类
numbersDiv.querySelectorAll('span').forEach(el => el.classList.remove('active'));
// 高亮目标行(注意:lineNumber 从 1 开始,索引从 0 开始)
const targetSpan = numbersDiv.children[lineNumber - 1];
if (targetSpan) targetSpan.classList.add('active');
}
// 绑定事件:覆盖所有光标变化场景
['input', 'click', 'keydown', 'focus'].forEach(evt => {
ta.addEventListener(evt, () => {
updateRowNumbering(); // 同步行数(新增/删除空行时必需)
const line = getCurrentLineNumber();
highlightLineNumber(line);
});
});
// 首次初始化
updateRowNumbering();
highlightLineNumber(getCurrentLineNumber());
</script>
⚠️ 注意事项与最佳实践
- 不要依赖 onkeyup="updateRowNumbering(this)":内联事件绑定耦合度高,且 keyup 无法捕获鼠标点击或粘贴后的光标跳转;应统一用 addEventListener;
- selectionStart 在 textarea 为空时为 0,此时行号恒为 1 —— 上述 getCurrentLineNumber() 已安全处理;
- CSS 中 .numbers span 必须设为 inline-block 或 block,否则 background-color 不会包裹整个行号区域;
- 若需支持滚动同步(如行号随内容滚动固定),建议改用 position: sticky 或虚拟滚动方案,超出本教程范围;
- 如需更高性能(万行级文本),可考虑缓存行偏移数组,避免每次遍历全文本。
该方案轻量、兼容性强(无需框架),可直接集成到任何基于










