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

如何在Chrome扩展中配合React使用react-shadow实现Grammarly式文本框悬浮元素

实现方案说明

你之前的实现核心问题在于:input/textarea 属于原生可替换元素,本身不支持直接挂载 Shadow DOM,直接调用 attachShadow 会静默失败,也会影响输入框本身的原生行为。

Grammarly的核心实现逻辑可以拆解为以下几点:

  • 不修改原页面的输入框元素,而是单独创建一个自定义DOM节点作为Shadow DOM的宿主,挂载到document.body下,完全和原页面DOM隔离,避免被原页面样式影响
  • 实时监听当前激活的输入框状态,通过getBoundingClientRect()获取输入框的视口坐标,动态计算悬浮元素的定位值
  • 监听页面滚动、窗口缩放、输入框尺寸变化、焦点切换等事件,实时更新悬浮元素的位置,保证始终对齐在输入框的指定位置

原生JS基础实现示例

// 1. 创建Shadow宿主节点,挂载到body
const shadowHost = document.createElement('div');
// 宿主本身设置fixed定位,层级拉满,不影响原页面布局
shadowHost.style.cssText = `
  position: fixed;
  z-index: 99999;
  pointer-events: none;
`;
document.body.appendChild(shadowHost);

// 2. 挂载Shadow DOM
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });

// 3. 创建悬浮元素,写入样式和内容
const floatingBtn = document.createElement('div');
floatingBtn.className = 'grammarly-btn';
floatingBtn.textContent = '悬浮按钮';
floatingBtn.style.cssText = `
  position: absolute;
  width: 32px;
  height: 32px;
  background: #16a34a;
  border-radius: 50%;
  pointer-events: all;
  cursor: pointer;
`;
// Shadow内部样式隔离
const style = document.createElement('style');
style.textContent = `
  .grammarly-btn:hover {
    background: #15803d;
  }
`;
shadowRoot.appendChild(style);
shadowRoot.appendChild(floatingBtn);

// 4. 定位更新函数
function updatePosition(targetInput) {
  if (!targetInput || !['INPUT', 'TEXTAREA'].includes(targetInput.tagName)) {
    shadowHost.style.display = 'none';
    return;
  }
  shadowHost.style.display = 'block';
  const rect = targetInput.getBoundingClientRect();
  // 对齐到输入框右下角,预留4px边距
  const left = rect.left + rect.width - 32 - 4;
  const top = rect.top + rect.height - 32 - 4;
  shadowHost.style.left = `${left}px`;
  shadowHost.style.top = `${top}px`;
}

// 5. 绑定事件监听
document.addEventListener('focusin', (e) => updatePosition(e.target));
document.addEventListener('focusout', () => shadowHost.style.display = 'none');
window.addEventListener('scroll', () => updatePosition(document.activeElement), true);
window.addEventListener('resize', () => updatePosition(document.activeElement));
// 监听输入框内容变化导致的尺寸变化
document.addEventListener('input', (e) => {
  if (e.target.tagName === 'TEXTAREA') updatePosition(e.target);
});

React适配方案

不需要使用第三方react-shadow包,直接用React原生的createPortal就能把组件渲染到Shadow DOM内部:

import { createPortal, useEffect, useState } from 'react';

// 悬浮按钮组件
function FloatingGrammarlyBtn() {
  const [shadowRoot, setShadowRoot] = useState(null);

  useEffect(() => {
    // 初始化创建Shadow宿主
    const host = document.createElement('div');
    host.style.cssText = `
      position: fixed;
      z-index: 99999;
      pointer-events: none;
    `;
    document.body.appendChild(host);
    const root = host.attachShadow({ mode: 'open' });
    setShadowRoot(root);

    // 事件监听逻辑和上面原生实现一致
    return () => host.remove();
  }, []);

  if (!shadowRoot) return null;

  return createPortal(
    <>
      <style>
        {`
          .floating-btn {
            width: 32px;
            height: 32px;
            background: #16a34a;
            border-radius: 50%;
            pointer-events: all;
            cursor: pointer;
          }
          .floating-btn:hover {
            background: #15803d;
          }
        `}
      </style>
      <div className="floating-btn">悬浮</div>
    </>,
    shadowRoot
  );
}

内容的提问来源于stack exchange,提问作者user17085802

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 06:24:07