You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

纯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(" ", "&nbsp;");
    } 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);

方案说明

  1. 样式同步:让overlay继承textarea的所有文本相关样式(字体、内边距、行高等),并设置white-space: pre-wrap和word-wrap: break-word,确保文本换行、空格处理和textarea完全一致。
  2. 自动位置计算:不再手动计算偏移量,而是将每个单词(含空格)放在span中,错误单词的下划线作为span的子元素,利用相对定位自动附着在单词下方,浏览器会自动处理换行后的位置。
  3. 交互处理:给下划线元素设置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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 02:49:54