现代化HTML
原生折叠面板
<details>
原生折叠面板
<details>+<summary>
做一个“点击展开/收起”的 FAQ 列表,以前要写 JS 监听 click 事件,切换 class,现在只需要
<details>
<summary>点击查看更多详情</summary>
<p>这里是隐藏的内容!可以是图片、文字或任何东西。</p>
<p>只要给 details 加一个 open 属性,默认就是展开的。</p>
</details>CSS 配合: 你可以用 details[open] 选择器来做展开动画!
1. 基本语法
<details>
<summary>点击展开/收起</summary>
<p>这里是展开后显示的内容。</p>
</details>浏览器会自动渲染一个可点击的箭头图标,点击 <summary> 就能切换内容的显示/隐藏。

2. 重要特性
| 元素 / 属性 | 作用 |
|---|---|
<details> | 容器元素,包裹要展开/折叠的内容 |
<summary> | 可点击标题,控制展开/收起 |
open 属性 | 控制默认是否展开,等同于 <details open> |
toggle 事件 | 当展开状态变化时触发,可用 JS 监听 |
3. 示例:默认展开 + JS 监听
<details open>
<summary>展开的详情</summary>
<p>默认就是展开的,因为加了 open 属性。</p>
</details>
<script>
const details = document.querySelector('details');
details.addEventListener('toggle', () => {
console.log('当前状态:', details.open ? '展开' : '收起');
});
</script>4. 样式自定义
默认浏览器会加一个小箭头,可以通过 CSS 自定义样式:
details summary {
cursor: pointer;
font-weight: bold;
list-style: none; /* 去掉默认小箭头 */
}
details[open] summary {
color: #0070f3;
}想要完全自定义箭头,可以隐藏默认标记:
summary::-webkit-details-marker {
display: none; /* 去掉默认 marker */
}然后自己加个图标或伪元素模拟展开/收起效果。
5. 特点和优势
✅ 零 JS:自带交互逻辑
✅ 无障碍支持:自动处理键盘导航和 ARIA 属性
✅ 可嵌套:可以多个 <details> 组成折叠列表
✅ 状态可控:用 JS 改 open 属性就能展开/收起
来试着美化成这样吧:

<dialog>
<dialog>是原生模态弹窗,以前做一个 Modal(弹窗),要处理 z-index、遮罩层、点击空白关闭、ESC 关闭……太累了。
原生 HTML5 <dialog> 元素 是浏览器内置的对话框组件,用来显示模态窗口或非模态弹出层,无需额外的 JS UI 库就能实现基本弹窗功能。
基本用法
<dialog id="myDialog">
<p>这是一个原生对话框</p>
<button id="closeBtn">关闭</button>
</dialog>
<button id="openBtn">打开对话框</button>
<script>
const dialog = document.getElementById('myDialog');
document.getElementById('openBtn').addEventListener('click', () => dialog.showModal());
document.getElementById('closeBtn').addEventListener('click', () => dialog.close());
</script>
关键特性
| 特性/方法 | 作用 |
|---|---|
<dialog> | 定义一个对话框元素,默认是 display: none |
open 属性 | 表示对话框是否打开,手动加上 open 就能显示 |
show() | 打开对话框(非模态,页面其他元素仍可交互) |
showModal() | 打开对话框(模态,阻止背景交互,并自动添加遮罩层) |
close() | 关闭对话框 |
::backdrop | 伪元素,可自定义遮罩层样式 |
returnValue | 可存储关闭时返回的值 按下esc就是返回值为空 |
样式自定义
你可以用 CSS 美化 <dialog> 和遮罩层:
dialog {
border: none;
border-radius: 10px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.5); /* 自定义遮罩 */
}注意事项
兼容性:现代浏览器都支持(IE 不支持)。
<dialog> 默认是脱离文档流的,不会占空间。
如果要在表单里用,可以直接用 <form method="dialog">,关闭时表单值会被传给 returnValue。
<dialog id="formDialog" open>
<form method="dialog">
<p><label>名字:<input name="username"></label></p>
<menu>
<button value="cancel">取消</button>
<button value="confirm">确定</button>
</menu>
</form>
</dialog>
<script>
const dlg = document.getElementById("formDialog");
dlg.addEventListener("close", () => {
console.log("用户操作:", dlg.returnValue);
});
</script>
<datalist>
<datalist>就是原生自动补全,之前你想给输入框加一个“推荐选项”,但用户也能自己输别的。以前要用 Select2 这种重型插件。现在只需要给 Input 挂一个数据列表。
<label>选择你的浏览器:</label>
<input list="browsers" name="browser">
<datalist id="browsers">
<option value="Chrome">
<option value="Firefox">
<option value="Safari">
<option value="Edge">
</datalist>效果: 用户点击输入框,会弹出下拉列表;用户输入 “Ch”,会自动筛选出 “Chrome”。零 JS!
<fieldset>
<fieldset> 是 HTML 原生的表单分组容器,用来把一组表单控件(input、select、textarea…)组织在一起,通常搭配 <legend> 元素作为分组标题。
它的主要作用是:
- 语义化:告诉浏览器和无障碍工具“这是一组相关的表单控件”
- 视觉样式:默认会画一个边框把这组控件框起来
- 可禁用:可以一次性禁用整组表单控件
1. 基本用法
<form>
<fieldset>
<legend>个人信息</legend>
<label>
姓名:<input type="text" name="name">
</label>
<label>
邮箱:<input type="email" name="email">
</label>
</fieldset>
</form>
浏览器会自动渲染:
- 外层一个边框框住整个
fieldset legend会显示在边框上方作为标题
2. 主要属性
| 属性 | 作用 |
|---|---|
disabled | 禁用整个 fieldset 内的控件(不可编辑、不可提交) |
form | 指定此 fieldset 属于哪个表单(即便不在 <form> 标签里) |
name | 给 fieldset 命名,便于在 JS 或表单数据里识别 |
示例:禁用整个分组
<fieldset disabled>
<legend>支付方式</legend>
<label><input type="radio" name="pay" value="card">信用卡</label>
<label><input type="radio" name="pay" value="cash">现金</label>
</fieldset>所有单选按钮都会被禁用。
3. 样式自定义
你可以完全自定义外观,不一定要用默认边框:
fieldset {
border: 2px solid #ccc;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
legend {
font-weight: bold;
padding: 0 0.5rem;
}也可以用 border: none; 去掉默认边框,把它当普通分组容器使用。

4. 特点和优势
✅ 无障碍友好:屏幕阅读器会把它作为一组控件一起朗读 ✅ 批量控制:可以一次禁用/启用整组表单控件 ✅ 语义清晰:代码可读性更高,特别是表单很复杂时
5. 对比常见替代方案
- 如果只是做视觉分组,可以用
<div>+ CSS - 如果需要无障碍支持和批量禁用功能,最好用
<fieldset>
Popover
你之前学了 <dialog>(模态框,强制遮罩)。但如果你只是想做一个 “点击按钮,旁边弹出一个小气泡/菜单” 呢?
以前要做一个 Tooltip 或 Dropdown,你得算坐标 (top: 100px; left: 200px),还得处理 z-index,还得监听“点击空白处关闭”。现在只需要使用原生 popover 属性。
popover 是 HTML 新增的一个属性/特性,用来实现原生的轻量弹出层(popover),类似 tooltip、dropdown、menu 这类浮层 UI,完全不需要自己写 JS 计算定位。它是继 <dialog> 之后,浏览器在原生层面提供的另一个“UI 基础能力”。
1. 基本概念
popover 不是一个新标签,而是一个属性,可以加在任意元素上,使该元素变成可显示/隐藏的浮层。
基本用法
<button popovertarget="my-popover">打开 Popover</button>
<div popover id="my-popover">
<p>这是一个原生 popover!</p>
</div>点击按钮后,浏览器会自动切换 #my-popover 的可见状态。

点击空白就关闭了这个popover
2. 核心属性
| 属性/方法 | 作用 |
|---|---|
popover | 让元素成为 popover 浮层,默认是隐藏的 |
popovertarget | 绑定触发器,指定要控制的 popover 元素的 id |
popovertargetaction | 控制行为,可选值:toggle(默认)、show、hide |
:popover-open | CSS 伪类,当 popover 打开时匹配,可用来写动画 |
.showPopover() / .hidePopover() | JS API,手动打开/关闭 popover |
.togglePopover() | JS API,手动切换 popover 状态 |
3. 示例:不同动作
<button popovertarget="p1" popovertargetaction="show">显示</button>
<button popovertarget="p1" popovertargetaction="hide">隐藏</button>
<button popovertarget="p1" popovertargetaction="toggle">切换</button>
<div popover id="p1" class="p-3 border rounded">
我是 popover
</div>4. 样式与动画
popover 本质上是一个元素,默认样式由浏览器管理,你可以自己写 CSS:
[popover] {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 8px 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
opacity: 0;
transition: opacity 0.2s ease;
}
[popover]:popover-open {
opacity: 1; /* 打开时渐显 */
}5. 特点与优势
✅ 原生定位:自动计算位置,避免自己写 position: absolute; left/top
✅ 原生状态管理:不需要 JS 维护开关逻辑,浏览器帮你处理
✅ 无障碍支持好:自动管理焦点和可访问性
✅ 比 <dialog> 更轻量:适合 tooltip、菜单、气泡提示
✅ 可嵌套:一个页面可以有多个 popover,相互独立
6. 注意事项
- 浏览器支持还在推进中,2025 年主流 Chrome、Edge、Safari 都支持,Firefox 也在跟进。
- 不像
<dialog>自带遮罩层,popover 不会阻止背景点击,更适合非模态场景。 - 可以和
<form>、menu、<button>配合做原生 dropdown / context menu。
图片与多媒体
<img src="..."> 已经不够用了。现代 HTML 要求我们根据设备环境提供不同的资源。
<picture>
电脑上:显示一张宽屏的风景图。
手机上:如果只是把宽图缩小,根本看不清细节。你想换成一张正方形的特写图。
<picture>
<source media="(max-width: 600px)" srcset="portrait.jpg">
<source srcset="img.avif" type="image/avif">
<img src="landscape.jpg" alt="风景图">
</picture>底蕴价值: 这叫 Art Direction (艺术指导)。浏览器只下载其中一张图片,不会浪费流量。
<figure> & <figcaption>
这俩是语义化图注:
图片下面加一行小字说明,以前是 <div class="img-wrap"><img...><span class="desc">...</span></div>。浏览器不知道这行字是属于这张图的。
<figure>
<img src="logo.png" alt="Logo">
<figcaption>图1:公司 2024 年新版 Logo 设计</figcaption>
</figure>懒加载
原生懒加载
<img loading="lazy">
页面有 100 张图片。用户刚打开页面,浏览器就傻乎乎地把 100 张全下载了,导致首屏加载巨慢。
以前的做法:引入 lozad.js 或写 IntersectionObserver 监听滚动。
现在只需要一个属性就解决了
<img src="heavy-image.jpg" loading="lazy" alt="...">
<img src="..." decoding="async">资源预加载:
<link rel="preload">
你的网页用了一个特殊的字体文件。通常浏览器解析到 CSS 里的 @font-face 才会去下载字体,这时候字会闪一下。
告诉浏览器:“这个东西很重要,别等了,先去下载!”
<head>
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
</head>原生输入控件
还在用 jQuery UI Datepicker 吗?还在用 JS 写颜色选择器吗?快停下!
原生拾色器与日历
<input type="...">
HTML5 的 input 类型已经进化得非常强大,尤其在手机端,它们会调用系统原生的滚轮选择器,体验比 JS 插件好一万倍。
<input type="color" value="#ff0000">
<input type="date">
<input type="datetime-local">
<input type="range" min="0" max="100" step="10">底蕴价值: 这些控件最大的优势是 Accessibility (无障碍) 和 Mobile Friendly。在手机上,原生的日期滚轮比任何网页模拟的日历都好用。
网页变成编辑器
你想做一个像 Notion 或在线文档那样的编辑器吗?
contenteditable
你想让用户直接在 <div> 里打字,而不是在丑陋的 <textarea> 里。
<div contenteditable="true" class="editor">
这里的内容可以直接修改...
<b>甚至可以加粗</b>
</div>spellcheck
浏览器会认为代码里的变量名(如 myVar)是拼写错误的单词,并在下面画红色波浪线。这很烦人。
<textarea spellcheck="false">const myVar = 1;</t键盘模式
当你让用户输入“验证码(数字)”时,手机弹出的却是“全键盘(带字母)”。用户得手动切换到数字键盘,体验极差。
现在可以使用inputmode强制弹起指定键盘。
<input type="text" inputmode="numeric" pattern="[0-9]*">
<input type="tel" inputmode="tel">
<input type="email" inputmode="email">
<input type="url" inputmode="url">enterkeyhint
在聊天框里,键盘右下角是“换行”。
在搜索框里,键盘右下角是“前往”。
如果不控制,用户经常因为不知道点哪个而困惑。
现代解法: 自定义那个蓝色按钮的文案/图标。
<input type="text" enterkeyhint="send">
<input type="search" enterkeyhint="search">
<input type="text" enterkeyhint="done">短信验证码的“自动填充”
autocomplete="one-time-code"
场景: 用户用手机号登录,收到了短信验证码 123456。
新体验:
- 短信来了。
- 键盘上方自动出现“123456 来自短信”。
- 用户点一下,自动填入。
<label>请输入验证码:</label>
<input
type="text"
inputmode="numeric"
autocomplete="one-time-code"
pattern="\d{6}"
>底蕴价值: 这行代码直接打通了 操作系统(iOS/Android) 和 浏览器。它让 Web 的登录流程丝滑程度追平了原生 App。
调用摄像头
想做“拍照上传”功能?千万别只写 type="file"。
capture
痛点: 在手机上点 <input type="file">,默认是弹出一个菜单:“相册、文件、拍照”。用户想自拍还要多点一次。
现代解法: 直接进相机。
<input type="file" accept="image/*" capture="user">
<input type="file" accept="image/*" capture="environment">
<input type="file" accept="video/*" capture="environment">体验提升: 这在做 实名认证(拍身份证) 或 头像上传 时非常关键。少一步操作,转化率就高一分。
语义化搜索
你还在用 <div> 包裹 <input> 来做搜索框吗?HTML5 终于给了它名分。
<search>
<form action="/search">
<input type="search" placeholder="搜索...">
<button type="submit">Go</button>
</form>
</search>底蕴价值: 除了语义清晰,type="search" 在手机键盘上会自动把“回车”变成“搜索”,而且那个自带的 “一键清除按钮” (X) 是浏览器原生提供的,既美观又好用。
体验优化
这是一个极其前沿但超级实用的属性(Chrome 已支持)。
自动展开搜索:hidden="until-found"
你用 <details> 做了一个很长的 FAQ 页面,内容都是折叠的。用户按 Ctrl + F (页面内查找) 搜关键词。结果: 搜不到!因为内容被折叠隐藏了。用户必须手动一个个点开才能搜。
<details>
<summary>如何退款?</summary>
<p hidden="until-found">
退款流程是:点击订单,选择售后...
</p>
</details>神奇效果: 当用户按 Ctrl + F 搜索“售后”时,浏览器会自动强制展开这个 <details> 面板,并高亮“售后”两个字!
沉浸式体验
你有没有发现,有些网站在手机上打开时,**顶部的系统状态栏(电量、时间那一栏)**会自动变成和网页一样的颜色?而不是默认的白色或灰色。
<meta name="theme-color">
- 痛点: 网页做得再漂亮,顶着一个白色的浏览器状态栏,瞬间就有种“我在刷网页”的出戏感。
- 现代解法: 把浏览器 UI 染成你的品牌色。
<meta name="theme-color" content="#4285f4">
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#4285f4" media="(prefers-color-scheme: light)">底蕴价值: 这叫 Web App Manifest 的一部分。它消除了“网页”和“原生应用”在视觉上的边界,是 PWA(渐进式 Web 应用)的第一步。
强制下载
<a download>
你放了一个 PDF 或图片链接 <a href="report.pdf">下载报告</a>。用户点击后,浏览器直接在当前页打开了这个 PDF。用户想下载,还得右键 -> 另存为… 体验很差。
<a href="report.pdf" download>下载报告</a>
<a href="/files/x8s7f.pdf" download="2024年度财报.pdf">
下载并重命名
</a>让链接变“卡片”
为什么把淘宝或 B 站的链接发到微信/Twitter/Slack 里,会变成一张漂亮的卡片(有图、有标题、有简介),而你的链接发出去只是一行蓝色的字?
Open Graph 协议<meta property="og:...">
- 痛点: 链接裸奔,没人想点。
- 现代解法: 告诉社交软件,怎么渲染你的网页。
<head>
<meta property="og:title" content="惊爆!HTML 还能这么写?">
<meta property="og:description" content="5 个你绝对不知道的 HTML 标签...">
<meta property="og:image" content="https://example.com/cover.jpg">
<meta property="og:type" content="article">
</head>体验提升: 这直接决定了你的网页在社交网络上的点击率 (CTR)。一张好图胜过千言万语。
交互冻结inert
场景: 侧边栏打开时,你希望背后的主页面完全不可点、不可选、不可聚焦。
<main id="content" inert>
...这里的所有按钮都点不了,文字选不中,Tab 键也会跳过...
</main>
<aside id="sidebar">
...现在用户只能在这里操作...
</aside>视频播放
你有没有试过在手机网页上放一个 <video> 作为背景? 结果: 视频全是黑的,或者点一下才能播,或者一播就自动全屏盖住了网页。
手机浏览器为了省流量,默认禁止自动播放视频。 必须集齐 3 个属性,才能召唤神龙。
<video autoplay muted playsinline src="bg.mp4"></video>autoplay: 自动播放。muted: 关键! 只有静音的视频,浏览器才允许自动播放。playsinline: 关键! 在 iOS 上,视频默认会弹出来全屏播放。加上这个属性,视频就会乖乖地“内嵌”在网页里播放(像 GIF 一样)。
原生正则校验:pattern
场景: 输入用户名,要求:只能是字母或数字,且 6-12 位。
<form>
<label>用户名:</label>
<input
type="text"
pattern="[a-zA-Z0-9]{6,12}"
title="请输入6-12位字母或数字"
required
>
<button>提交</button>
</form>
<style>
/* 配合 CSS:输入不对时变红 */
input:invalid {
border-color: red;
}
input:valid {
border-color: green;
}
</style>体验提升: 当用户输入不符合规则并点击提交时,浏览器会自动弹出一个气泡提示(内容就是你写的 title),并阻止表单提交。零 JS!
user-invalid这个CSS伪元素可以增强体验
Web Components
核心概念:Web Components 的“三驾马车”
Web Components 不是一个单一的技术,而是三项技术的组合:
- Custom Elements(自定义元素): 允许你定义
<my-button>这样的新 HTML 标签。 - Shadow DOM(影子 DOM): 提供样式和结构的隔离。你的组件内部样式不会影响外面,外面的 CSS 也搞不坏你的组件。(这是 Web Components 最强的地方)。
- HTML Templates(模板): 使用
<template>和<slot>来定义组件的结构,这点和 Vue 非常像。
实战演练:写一个 <user-card> 组件
我们要实现一个用户卡片组件,功能如下:
- 支持传入
name和avatar属性。 - 支持插槽(Slot)放入自定义内容。
- 样式隔离,不被全局 CSS 污染。
第一步:HTML 结构(怎么用它)
我们希望最终能这样使用它:
<user-card
name="Gemini"
avatar="https://api.dicebear.com/7.x/avataaars/svg?seed=Gemini"
>
<p slot="bio">我是 Google 的 AI 模型。</p>
<button>关注我</button>
</user-card>第二步:JavaScript 实现(怎么造它)
不需要任何构建工具(Webpack/Vite),新建一个 .html 文件就能跑。
// 1. 定义一个类,继承自 HTMLElement
class UserCard extends HTMLElement {
// 构造函数:初始化组件
constructor() {
super(); // 必须第一行调用
// 🌟 核心:开启 Shadow DOM
// mode: 'open' 意味着你可以通过 JS (this.shadowRoot) 访问内部
this.attachShadow({ mode: 'open' });
// 渲染组件结构
this.render();
}
// 2. 监听属性变化:告诉浏览器我们要监听 'name' and 'avatar'
static get observedAttributes() {
return ['name', 'avatar'];
}
// 3. 生命周期:当属性变化时触发(类似 Vue 的 watch)
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
this.render(); // 重新渲染
}
// 4. 渲染函数
render() {
// 获取属性值
const name = this.getAttribute('name') || '匿名用户';
const avatar = this.getAttribute('avatar') || 'default.png';
// 🌟 写入 Shadow DOM
// 注意:这里的 <style> 只在这个组件内部生效!
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block; /* 默认是 inline,改一下 */
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
width: 300px;
font-family: sans-serif;
background: #fff;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.header { display: flex; align-items: center; gap: 10px; }
img { width: 50px; height: 50px; border-radius: 50%; }
h3 { margin: 0; color: #333; }
.content { margin-top: 10px; color: #666; font-size: 0.9em; }
/* ::slotted 可以给插进来的内容设置样式 */
::slotted(button) {
background: #007bff;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
</style>
<div class="header">
<img src="${avatar}" alt="Avatar">
<h3>${name}</h3>
</div>
<div class="content">
<slot name="bio">暂无简介</slot>
<hr/>
<slot></slot>
</div>
`;
}
}
// 5. 注册组件:把类和标签名绑定
// ⚠️ 标签名必须包含连字符 (-),不能叫 <card>,必须叫 <user-card>
customElements.define('user-card', UserCard);进阶:生命周期(Lifecycle)
Web Components 的生命周期非常简单,一共只有 4 个回调:
constructor(): 组件被创建(内存中)时执行。初始化数据。connectedCallback(): 重要! 组件被插入到页面 DOM 时执行。- Vue 对应:
onMounted - 用途: 发起网络请求、绑定事件监听、启动定时器。
- Vue 对应:
disconnectedCallback(): 组件从页面被移除时执行。- Vue 对应:
onUnmounted - 用途: 清理定时器、解绑事件(防止内存泄漏)。
- Vue 对应:
attributeChangedCallback(name, oldVal, newVal): 属性变化时执行。- Vue 对应:
watch - 用途: 响应数据变化更新视图。
- Vue 对应:
示例:给组件加个点击事件
connectedCallback() {
console.log('组件已挂载!');
// 给内部的头像绑定点击事件
this.shadowRoot.querySelector('img').addEventListener('click', () => {
alert('你点了头像!');
// 🌟 发送自定义事件给外部(父组件)
// detail 里面可以放任何数据
this.dispatchEvent(new CustomEvent('avatar-click', {
detail: { userId: 123 },
bubbles: true, // 允许冒泡
composed: true // 允许穿透 Shadow DOM 边界传出去
}));
});
}痛点与现代解决方案:Lit
写原生 Web Components 有个问题:太啰嗦了! 你看上面的 render() 函数,直接用字符串拼接 HTML,这非常容易出错,而且性能不好(每次都重写 innerHTML)。
所以在真实开发中(比如 Google 的 Material Design 团队),大家会用一个微型库:Lit。
Lit 是 Google 推出的 Web Components 封装库,它让写法变得非常像 React/Vue,但产出的依然是标准的 Web Components。
看一下 Lit 的写法(对比上面,清爽了多少):
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('user-card')
export class UserCard extends LitElement {
// 自动处理属性监听和类型转换
@property({ type: String }) name = '匿名';
@property({ type: String }) avatar = '';
// 样式(Scoped CSS)
static styles = css`
:host { display: block; padding: 16px; border: 1px solid #ccc; }
h3 { color: blue; }
`;
// 渲染(使用 html 模板字面量,类似 React JSX)
render() {
return html`
<div>
<img src="${this.avatar}">
<h3>${this.name}</h3>
<slot></slot>
</div>
`;
}
}评论
评论加载中……