前端这几年被 JS 框架”惯坏”了。想做个折叠面板?先装个组件库。想做个弹窗?再引入一个 modal 插件。想做个下拉多选?继续加依赖。
但很多时候,浏览器早就把这些功能做好了,只是我们没用而已。这篇文章就聊聊那些不用写一行 JavaScript、纯靠 HTML 标签和属性就能实现的能力。看完你会发现,有些代码是真的可以删了。
1. 折叠面板:<details> + <summary>
以前做手风琴(accordion)折叠效果,标配是一个点击事件 + 一段 display: none/block 的切换逻辑。现在直接用两个标签就搞定:
<details>
<summary>点击查看隐藏内容</summary>
<p>这里是被折叠起来的内容,点击标题就能展开或收起。</p>
</details>
<details> 是容器,<summary> 是可点击的标题栏,浏览器自动帮你处理展开收起的状态和小箭头图标。想默认展开,加个 open 属性即可:<details open>。
如果想做手风琴那种”同时只能展开一个”的效果,HTML 也支持了——给多个 <details> 设置同名的 name 属性:
<details name="faq">
<summary>问题一</summary>
<p>答案一</p>
</details>
<details name="faq">
<summary>问题二</summary>
<p>答案二</p>
</details>
这样点开一个,另一个会自动收起,跟单选按钮的逻辑一模一样。
2. 弹窗对话框:<dialog>
做过 Modal 组件的都知道,要处理的东西不少:遮罩层、居中定位、ESC 键关闭、焦点锁定(focus trap)……这些浏览器现在都内置了,用的就是 <dialog> 元素。
<dialog id="myDialog">
<p>这是一个原生弹窗</p>
<form method="dialog">
<button>关闭</button>
</form>
</dialog>
唯一需要一点点 JS 的是”打开”这个动作(dialog.showModal()),但关闭、遮罩、居中、ESC 键响应、焦点管理这些浏览器全包了。而且用 method="dialog" 的表单按钮点击后会自动关闭对话框,不需要额外绑定事件。
3. 表单校验:浏览器自带的”前端验证”
很多项目里,表单校验是靠 JS 库(比如某些校验插件)一条条写规则。但 HTML 的表单属性已经能覆盖大部分基础场景:
<form>
<input type="email" required placeholder="请输入邮箱">
<input type="number" min="1" max="100" required>
<input type="text" pattern="[A-Za-z]{3,}" title="至少3个字母">
<button type="submit">提交</button>
</form>
required:必填type="email":自动校验邮箱格式min/max:数字范围pattern:正则校验minlength/maxlength:长度限制
不满足条件时,浏览器会自动弹出提示气泡,阻止表单提交,完全不需要写校验函数。当然,生产环境一般还需要后端二次校验,但前端这层”防呆”完全可以交给浏览器。
4. 更丰富的输入控件
<input> 的 type 属性远比大多数人用到的多,很多场景不需要自己撸 UI 组件:
<input type="color"> <!-- 颜色选择器 -->
<input type="range"> <!-- 滑动条 -->
<input type="date"> <!-- 日期选择器 -->
<input type="datetime-local"> <!-- 日期+时间 -->
<input type="file" multiple> <!-- 多文件选择 -->
这些控件的样式和交互(比如日期弹出的小日历面板)都是浏览器原生渲染的,兼容性也已经相当不错。虽然样式在不同浏览器下略有差异,但功能上是完整的,很多后台管理系统的日期选择、颜色配置需求,其实用不着额外的组件库。
5. 输入建议列表:<datalist>
想做一个”输入时有下拉建议”的搜索框,通常第一反应是自己写个下拉列表组件。其实 HTML 有现成方案:
<input list="cityList" placeholder="输入城市">
<datalist id="cityList">
<option value="北京">
<option value="上海">
<option value="广州">
<option value="深圳">
</datalist>
用户输入时,浏览器会自动根据 datalist 里的内容过滤匹配项并展示下拉建议。适合简单的联想输入场景,不需要精细定制样式的话完全够用。
6. 弹出层新贵:popover 属性
这是相对较新的能力(主流浏览器已经支持),用来做提示气泡、下拉菜单这类”浮层”内容:
<button popovertarget="myPopover">打开提示</button>
<div id="myPopover" popover>
这是一个原生弹出层,点击外部会自动关闭。
</div>
popover 属性会让这个元素脱离正常文档流,浮在页面最上层显示,并且自带”点击外部自动关闭””按 ESC 关闭”的行为,这些以前都得自己写事件监听和 z-index 管理。
7. 图片自适应与懒加载
响应式图片以前得靠 JS 判断视口宽度动态换图,现在 <picture> 和 srcset 直接由浏览器决定加载哪张图:
<img src="small.jpg"
srcset="small.jpg 480w, large.jpg 1080w"
sizes="(max-width: 600px) 480px, 1080px"
loading="lazy"
alt="示例图片">
loading="lazy" 这一个属性,就能让图片在滚动到可视区域附近时才开始加载,完全不需要引入专门的图片懒加载库,也不用手写 IntersectionObserver。
8. 进度和度量:<progress> 与 <meter>
进度条和”仪表盘式”的数值展示,也有对应的原生标签:
<progress value="70" max="100"></progress>
<meter value="0.6" min="0" max="1">60%</meter>
<progress> 用于表示任务进度(比如上传进度),<meter> 用于表示某个范围内的数值(比如磁盘使用率、电量)。两者语义不同,浏览器也会用不同的默认样式区分它们。
9. 输出计算结果:<output>
配合表单做实时计算展示(比如两个数字相加、滑动条联动数值),<output> 是专门为此设计的标签,语义上比一个普通 <span> 更准确,屏幕阅读器也能更好地识别它是”计算结果”:
<form oninput="result.value = parseInt(a.value) + parseInt(b.value)">
<input type="number" id="a" value="0"> +
<input type="number" id="b" value="0"> =
<output name="result" for="a b">0</output>
</form>
这里确实用到了一点点 JS 做加法运算,但 <output> 本身的语义化标签能力是原生的。
10. 一些容易被忽略的小属性
除了上面这些”大件”,还有几个属性平时用得上,但很多人没注意到:
download:给<a>标签加上这个属性,点击链接就是下载文件而不是跳转打开,<a href="file.pdf" download>下载文件</a>。contenteditable:让任意元素变成可编辑区域,<div contenteditable="true">,相当于一个简易富文本编辑器的地基。inert:给一个区域加上inert,里面的所有元素都会失去焦点和交互能力,常用于弹窗打开时”冻结”背景内容,不用手动给每个子元素禁用事件。autofocus:页面加载完自动聚焦到某个输入框,不需要element.focus()。
为什么要关注这些原生能力
写这篇文章不是说”JS 框架没用了”,复杂的交互、状态管理、组件复用,该用框架还是得用。但很多简单场景——一个折叠面板、一个表单校验、一个弹窗——完全可以用浏览器原生能力解决,好处很实在:
- 代码量更少:少写逻辑,少踩 bug。
- 性能更好:浏览器原生实现通常比 JS 模拟的更高效,尤其是弹窗的焦点管理、表单校验这些细节逻辑。
- 无障碍支持更好:原生标签自带语义和键盘操作支持,比如
<dialog>的 Tab 焦点循环、<details>的键盘展开,这些是很多自己手写的组件容易忽略的。 - 维护成本更低:少一个依赖,就少一次因为库升级或废弃带来的麻烦。
下次做需求评审的时候,不妨先想一下:这个功能,浏览器是不是早就帮我做好了?