纯HTML/CSS/JS实现文本框带下划线与弹窗的拼写检查功能
实现类似Grammarly的textarea拼写检查下划线功能
问题概述
需要实现Grammarly风格的textarea拼写检查:输入错误单词时用下划线标记,点击下划线触发弹窗。采用覆盖层div方案,但无法准确计算单词位置,尤其是textarea自动换行导致的位置偏移问题。
现有代码
HTML
<div id="textarea-container"> <textarea id="textarea" oninput="updateOverlay()">a b</textarea> <div id="overlay-container"> <div id="overlay"></div> </div> </div>
CSS
#textarea-container { position: relative; width: 400px; font-size: medium; font-weight: 500; font-family: 'Courier New', Courier, monospace; } #textarea, #overlay-container { width: 100%; height: 200px; box-sizing: border-box; padding: 0px; font-size: 16px; resize: none; border: 0px solid #ccc; margin: 0; } #overlay-container { position: absolute; top: 0; left: 0; pointer-events: none; will-change: transform; } #overlay { position: relative; width: 100%; height: 100%; box-sizing: border-box; padding: 0px; margin: 0px; } .underline { position: absolute; border-bottom: 1px solid blue; }
JavaScript
function updateOverlay() { const textarea = document.getElementById('textarea'); const overlay = document.getElementById('overlay'); overlay.innerHTML = ''; const text = textarea.value; const wordsAndSpaces = text.split(/(\s+)/) console.log(wordsAndSpaces) let offsetLeft = 0; let offsetTop = 0; let lineHeight = parseFloat(window.getComputedStyle(textarea).lineHeight); for (let item of wordsAndSpaces) { const span = document.createElement('span'); if (item.includes(" ")) { span.innerHTML = item.replace(" ", " "); } else { span.textContent = item; } span.style.display = 'inline-block'; overlay.appendChild(span); const rect = span.getBoundingClientRect(); const width = rect.width; const height = rect.height; const left = offsetLeft + rect.left - 10; const top = offsetTop + rect.top; const underline = document.createElement('div'); underline.classList.add('underline'); underline.style.width = width + 'px'; underline.style.height = '1px'; underline.style.left = left + 'px'; underline.style.top = top + height - 10 + 'px'; overlay.appendChild(underline); offsetLeft += span.offsetWidth; if (offsetLeft >= textarea.clientWidth) { offsetLeft = 0; offsetTop += lineHeight; } overlay.removeChild(span); } } // Initial call to update overlay updateOverlay();
解决方案:同步样式+自动位置计算
核心思路是让覆盖层完全复刻textarea的文本渲染规则,由浏览器自动处理换行和位置,无需手动计算偏移。
修改后代码
CSS 调整
#textarea-container { position: relative; width: 400px; font-size: medium; font-weight: 500; font-family: 'Courier New', Courier, monospace; } #textarea, #overlay-container { width: 100%; height: 200px; box-sizing: border-box; padding: 8px; /* 统一padding,匹配textarea默认内边距 */ font-size: 16px; resize: none; border: 1px solid #ccc; margin: 0; } #overlay-container { position: absolute; top: 0; left: 0; pointer-events: none; } #overlay { position: relative; width: 100%; height: 100%; box-sizing: border-box; white-space: pre-wrap; /* 和textarea一致的空格/换行处理 */ word-wrap: break-word; /* 匹配textarea的长单词换行规则 */ overflow: hidden; } .misspelled-word { position: relative; } .underline { position: absolute; bottom: 2px; left: 0; width: 100%; height: 1px; background-color: blue; pointer-events: auto; /* 允许点击交互 */ cursor: pointer; }
JavaScript 调整
function updateOverlay() { const textarea = document.getElementById('textarea'); const overlay = document.getElementById('overlay'); overlay.innerHTML = ''; // 让overlay完全继承textarea的文本样式,确保渲染一致 const textStyle = window.getComputedStyle(textarea); overlay.style.font = textStyle.font; overlay.style.padding = textStyle.padding; overlay.style.lineHeight = textStyle.lineHeight; overlay.style.width = textStyle.width; overlay.style.height = textStyle.height; overlay.style.boxSizing = textStyle.boxSizing; const text = textarea.value; // 分割单词和空格,保留原始格式 const wordsAndSpaces = text.split(/(\s+)/); for (const item of wordsAndSpaces) { const span = document.createElement('span'); span.textContent = item; // 模拟拼写检查逻辑:替换为真实的拼写检测接口即可 const isMisspelled = item.trim() && item.length > 3; if (isMisspelled) { span.classList.add('misspelled-word'); // 创建下划线元素并绑定点击事件 const underline = document.createElement('div'); underline.classList.add('underline'); underline.addEventListener('click', () => { // 替换为你的弹窗逻辑 alert(`建议修正单词:${item.trim()}`); }); span.appendChild(underline); } overlay.appendChild(span); } } // 初始化+监听输入 updateOverlay(); document.getElementById('textarea').addEventListener('input', updateOverlay);
方案说明
- 样式同步:让overlay继承textarea的所有文本相关样式(字体、内边距、行高等),并设置
white-space: pre-wrap和word-wrap: break-word,确保文本换行、空格处理和textarea完全一致。 - 自动位置计算:不再手动计算偏移量,而是将每个单词(含空格)放在span中,错误单词的下划线作为span的子元素,利用相对定位自动附着在单词下方,浏览器会自动处理换行后的位置。
- 交互处理:给下划线元素设置
pointer-events: auto,在overlay整体禁止指针事件的情况下,仍能触发点击弹窗。
替代方案:使用contenteditable元素
如果覆盖层方案仍有兼容性问题,可以直接用contenteditable="true"的div替代textarea,直接在文本中给错误单词添加样式化span,无需覆盖层:
示例代码
<div id="editor" contenteditable="true" style="width:400px;height:200px;padding:8px;border:1px solid #ccc;font-family:'Courier New', monospace;font-size:16px;">a b</div>
function checkSpelling() { const editor = document.getElementById('editor'); const text = editor.textContent; const wordsAndSpaces = text.split(/(\s+)/); editor.innerHTML = ''; for (const item of wordsAndSpaces) { const span = document.createElement('span'); span.textContent = item; const isMisspelled = item.trim() && item.length > 3; if (isMisspelled) { span.style.borderBottom = '1px solid blue'; span.style.cursor = 'pointer'; span.addEventListener('click', () => { alert(`建议修正单词:${item.trim()}`); }); } editor.appendChild(span); } } // 监听输入(需额外处理光标位置,避免输入时光标跳走) document.getElementById('editor').addEventListener('input', checkSpelling); checkSpelling();
方案优缺点
- 优点:无需处理覆盖层位置同步,样式和交互更直观。
- 缺点:需要手动处理textarea原生功能(如光标定位、粘贴格式、回车键换行等),开发成本更高。
内容的提问来源于stack exchange,提问作者Hahunavth
相关产品推荐
相关产品推荐

