POST://custom-modal-design
带点橘子味的馒头的头像
带点橘子味的馒头

FRONTEND / DESKTOP DEV

返回文章列表

LAN 聊天系统改造手记:从原生 confirm 到自定义模态框

1 分钟· 719 63 次阅读

聊天的项目里之前到处是 confirm(),用户反馈"不同浏览器弹窗长得不一样""点快了不知道点了哪个按钮"。于是系统性替换成自定义模态框,这篇文章记录设计与实现。

为什么不用原生 confirm

原生 confirm()/alert() 有三个硬伤:

  1. 样式不可控:每个浏览器/操作系统长得都不一样,和产品 UI 割裂;
  2. 阻塞主线程:弹窗期间页面完全冻结,连 CSS 动画都停了;
  3. 无返回值状态:只能拿到 true/false,拿不到"用户点了哪个按钮"的上下文。

自定义模态框的设计

基础结构

<div class="modal-backdrop" role="presentation">
  <div
    role="dialog"
    aria-modal="true"
    aria-labelledby="modal-title"
    aria-describedby="modal-desc"
  >
    <h2 id="modal-title">确认删除?</h2>
    <p id="modal-desc">删除后不可恢复。</p>
    <div class="modal-actions">
      <button data-action="cancel">取消</button>
      <button data-action="confirm" class="danger">删除</button>
    </div>
  </div>
</div>

关键细节

焦点管理:打开时把焦点移到对话框内的确认按钮;关闭后把焦点还给触发按钮。否则键盘用户会"丢焦点"。

export function showConfirm({ title, message, onConfirm }) {
  const trigger = document.activeElement;
  // ...创建 DOM、挂载...
  confirmBtn.focus();

  return {
    close(result) {
      // 卸载 DOM 后把焦点还给触发元素
      trigger?.focus();
      if (result) onConfirm?.();
    },
  };
}

Esc 与遮罩点击:Esc 等于取消,点遮罩也等于取消(但要有明确视觉反馈)。

document.addEventListener("keydown", (e) => {
  if (e.key === "Escape") modal.close(false);
});

动画与过渡:模态框打开用淡入 + 轻微上移,关闭时要有"正在关闭"的状态,避免瞬间消失的生硬感。用 display 切换配合 requestAnimationFrame 触发过渡,而不是直接隐藏。

跨浏览器一致性

自定义模态框天然解决了"不同浏览器长得不一样"的问题——所有浏览器渲染同一套 HTML/CSS。需要额外注意的是:

  • navigator.clipboard 等 API 的兼容性差异,模态框内容里的交互要用降级方案;
  • 移动端 <select> 等原生控件在模态框内的表现;
  • 长文本时模态框最大高度与滚动(max-height: 80dvh; overflow-y: auto)。

替换策略

不一次性全换,按风险从低到高分三批:

  1. 无副作用的确认框(删除、清空)—— 直接替换;
  2. 带输入的提示框 —— 换成自定义表单模态框;
  3. 异步操作结果反馈 —— 换成 toast 而非弹窗。

每批替换后跑一遍冒烟测试,重点是"取消/确认后焦点位置正确"和"Esc 行为一致"。

小结

自定义模态框不是"为了好看",它同时解决了一致性、可访问性和交互反馈三个问题。核心工作量不在样式,而在焦点管理、Esc 处理、动画过渡这三个细节——做对了,体验和原生一样顺手,但样式完全可控。

相关文章