首页 / 文章

六种可替代常见JavaScript UI库的原生HTML功能

了解弹出框、专属详情、对话框、声明式阴影 DOM、fetchpriority 以及 datalist 如何替代自定义 JavaScript,以及它们各自仍存在的局限。

发布者:REACTAPP.TOP阅读约 6 分钟1277 词

来源: HTML Features in 2026 That Most Devs Don’t Know Exist

多年来,HTML负责提供结构,而JavaScript则处理所有交互功能:模态框需要React库,工具提示则需要Popper.js或Tippy,服务器端渲染的组件则依赖自定义脚本。与此同时,WHATWG HTML Living Standard以及各类浏览器引擎新增了原生构建模块,仅需少量属性即可解决许多此类问题,既减少了冗余代码和文件大小,又能默认实现无障碍访问。本指南将介绍其中的六种功能、每种功能所需的标记,以及何时仍需要使用CSS或JavaScript。在采用备用方案之前,请先确认目标浏览器对其的支持情况。

1. 使用popover属性的弹出框

传统上,工具提示和菜单需要处理z-index堆叠上下文的问题,并手动编写点击外部区域的监听代码。而借助Popover API,只要某个元素带有popover属性,它在打开时就会被提升到最上层,位于所有普通内容之上,且不受z-index的影响。如果为按钮设置popovertarget属性并指定该元素的id,即可控制其显示与隐藏:

<!-- The Trigger -->
<button popovertarget="my-menu">Toggle Menu</button>

<!-- The Popover Content -->
<div id="my-menu" popover>
  <p>I am natively managed! Press ESC or click outside to close me.</p>
</div>

无需任何JavaScript即可实现:

  • 声明式控制:按钮可用来打开和关闭popover。
  • 简单关闭方式:点击页面外部或按Esc键即可关闭popover。这适用于默认的popover="auto"模式;若设置为popover="manual",则由用户自行决定关闭方式。
  • 无需处理堆叠问题:打开的popover会显示在最上层,位于其他所有内容之上。

两个限制:API仅能控制元素的可见性与堆叠方式,无法决定其位置,因此要将菜单固定在触发元素上仍需使用CSS(传统的定位方式或更新但支持度较低的锚点定位)。此外,弹出框属于非模态元素,不会锁定焦点,因此需使用dialog来实现确认功能。

2. 带详细内容与名称的折叠面板

details和summary早已能够无需JavaScript即可实现可折叠内容区域。新增功能是:具有相同name属性的details元素会组成一个组,该组中同一时间只能展开一个元素,功能类似传统的折叠面板。

<details name="faq">
  <summary>What is modern HTML?</summary>
  <p>A living standard evolving directly inside browser engines.</p>
</details>

<details name="faq">
  <summary>Do I still need JavaScript?</summary>
  <p>Much less of it than you used to!</p>
</details>

<details name="faq">
  <summary>Is this supported natively?</summary>
  <p>Yes, across all major modern browser engines.</p>
</details>

打开某个带有name="faq"属性的元素时,其他所有相同名称的开放项都会自动关闭,这一过程无需任何监听器、状态钩子或类切换操作。对于不支持name属性的浏览器,则会允许多个项目同时保持开放状态,作为优雅的降级方案。

3. 使用 dialog 和 ::backdrop 实现无障碍模态框

自定义的 div 叠加层常常无法通过无障碍审核,因为合格的模态框必须将焦点限制在自身内部、遮挡其背后的页面,并在操作结束后将焦点恢复到触发按钮。dialog 元素可解决这一问题。下方的表单使用了 method="dialog",无论点击哪个按钮都会关闭对话框,且被点击按钮的 value 值将成为对话框的 returnValue:

<dialog id="user-modal">
  <h2>Confirm Action</h2>
  <p>Are you sure you want to delete this repository?</p>

  <!-- A form inside a dialog with method="dialog" closes it automatically -->
  <form method="dialog">
    <button value="cancel">Cancel</button>
    <button value="confirm">Delete</button>
  </form>
</dialog>

<button onclick="document.getElementById('user-modal').showModal()">
  Open Modal
</button>

那个变暗的叠加层就是通过纯 CSS 样式实现的 ::backdrop 伪元素:

/* Styling the native browser backdrop */
dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(4px);
}

showModal()会将对话框置于最上层,使页面其余部分无法交互,保持焦点在对话框内,并允许使用 Escape 键关闭;关闭后,当前浏览器会将焦点恢复到之前聚焦的元素。show()则相反,以非模态方式打开对话框,且没有这些保护机制。在 React 中,应通过 ref 调用 showModal(),而非使用内联的 onclick。

4. 使用声明式 Shadow DOM 的服务器渲染 Web 组件

早期的 Shadow DOM 需要客户端 JavaScript(element.attachShadow({ mode: 'open' })),因此服务器渲染或静态生成的 Web 组件在脚本执行前没有封装的标记,从而导致页面出现无样式的闪烁和布局变动。声明式 Shadow DOM 解决了这一问题:在宿主元素中添加带有 shadowrootmode 的 template,即可在 HTML 中描述阴影树结构:

<user-card>
  <template shadowrootmode="open">
    <style>
      /* Styles here stay encapsulated inside this component */
      h3 { color: #3b82f6; font-family: sans-serif; }
    </style>

    <h3>Jane Doe</h3>
    <p>Senior Frontend Engineer</p>
  </template>
</user-card>

在渲染之前,解析器会将模板转换为user-card上的阴影根,这样作用域内的样式和标记就能在首次绘制时显示出来。需要注意的是:这一过程仅发生在HTML解析阶段。通过innerHTML在之后插入的标记不会生成阴影根;动态插入则需要使用如setHTMLUnsafe()之类的API,因此请确认当前是否支持。

5. 使用fetchpriority为关键资源设定优先级

loading="lazy"以及async/defer无法指示哪些早期加载的资源最为重要。当页面中有多张图片时,位于最大内容绘制点(LCP)之后的主图应比徽章和图标具有更高的优先级。fetchpriority可以为网络调度器提供这样的提示:

<!-- Critical LCP Image: Load this immediately! -->
<img src="hero-banner.webp" fetchpriority="high" alt="Product Hero">

<!-- Lower priority images -->
<img src="avatar.webp" fetchpriority="low" alt="User avatar">

<!-- Critical JavaScript bundle -->
<script src="app.js" fetchpriority="high"></script>

这无需修改应用程序逻辑即可提升核心网页指标。它只是建议而非强制要求,若将过多资源标记为高级别,则会抵消其优势。同时切勿对LCP图片使用懒加载。

6. 使用datalist的原生方案

许多项目会添加复杂的自动补全组件,仅用于在允许自由输入的同时提供选项建议。datalist可直接实现这一功能:通过输入框的list属性关联它,其选项就会成为建议内容。

<label for="framework">Choose or type a framework:</label>
<input list="frameworks" id="framework" name="framework" placeholder="e.g. Next.js">

<datalist id="frameworks">
  <option value="Next.js"></option>
  <option value="SvelteKit"></option>
  <option value="Nuxt.js"></option>
  <option value="Astro"></option>
  <option value="Remix"></option>
</datalist>

浏览器负责渲染和过滤列表,处理键盘导航,并遵循平台的视觉风格。相应的代价是控制力的缺失:样式极为有限,不同浏览器及移动设备上的行为存在差异,且无法实现丰富的选项功能。它非常适合简单的“选择或输入”类型字段;而需要品牌标识的下拉框仍需借助专用组件。

原生功能与旧有方法的对比

  • 工具提示与菜单:使用popover而非定位库,同时处理点击页面外部和按ESC键的情况(定位功能仍由CSS提供)。
  • 折叠面板:使用具有相同name属性的details元素,无需在JavaScript中实现状态管理与切换逻辑。
  • 模态框:使用dialog结合showModal()及::backdrop,而非自定义覆盖层并手动管理焦点。
  • 封装的服务器渲染组件:使用template shadowrootmode,而非仅在客户端调用的attachShadow()方法。
  • 加载优先级:使用fetchpriority,而非手动预加载技巧或通过脚本控制加载顺序。
  • 输入建议:在简单场景下,可使用 datalist 而非自定义自动补全组件。
  • 如需了解更多可替代依赖项的平台功能,可参阅我们关于原生浏览器 API 替代热门 npm 包的综述。

    核心要点

    • 现在的浏览器已内置了过去需要通过库才能实现的功能,通常还具备焦点处理和按 Escape 键关闭的功能。
    • “原生”并不意味着功能完备:弹出框需要 CSS 来定位,datalist 的样式选项有限,而声明式阴影 DOM 仅在解析时有效。
    • 在添加 UI 依赖项之前,先确认是否有某个元素或属性能满足你所支持的浏览器的实际需求。