title: "别再只说“弹个窗”:Vibe Coding 必会的 8 种浮层交互" date: "2026-10-25" excerpt: "用任务管理小案例分清 Toast、Drawer、Dialog 等 8 种交互:现场操作、实录动图、完整 HTML 与可复制 AI 提示词一起看。" tags: ["Vibe Coding", "UI交互", "前端开发", "教程"]
先说清楚:你希望用户完成什么
给 AI 一句“这里弹个窗”,它可能把保存提示、删除确认和编辑表单做成同一种弹框。界面能打开,操作却很别扭。先选对交互,再描述触发、内容、关闭方式和异常状态,生成结果才容易验收。
本文参考阿甘探 AI 的快手视频,沿用视频讲解的八类顺序,以及 Nordic Task Log 任务管理场景来解释用途。下方是本站独立编写的教学演示,不是原作者源码,也不声称视觉完全一致;提示词和验收要求是本文补充。
每节都可直接操作,并附浏览器实际操作录制的动图。展开演示下方的源码区,即可复制完整 HTML,保存为 .html 后用浏览器打开,无需安装依赖。演示只修改各自 iframe 的内存,刷新会重置,不会提交、删除或保存本站真实用户数据。
1. Toast:完成操作后,轻声告诉用户结果
适合“任务已完成”“设置已更新”这类短反馈。用户已做出决定,Toast 负责告知结果,不必要求再点一次确定。Radix Toast 文档也将它定位为临时短消息。
给 AI 的提示词:
在任务列表中加入完成操作。成功后显示 Toast,明确写出本次操作结果,短暂展示后消失,不抢输入焦点;提供关闭入口。反馈使用可被辅助技术感知的状态区域。连续点击要正确处理计时器,不能残留旧提示。
**亲手验收:**触发后继续操作任务列表;用键盘检查焦点没有跳走。手机上提示应完整可读。请求失败时不能弹“成功”,需要用户处理的错误也不能只闪一下就消失。
Toast · 轻提示
完成任务,观察状态变化,再试试撤销。
可直接操作。数据只在此演示区域临时变化,点击“重新开始”恢复初始状态。

查看完整可运行源码
与上方演示使用相同源码,无额外依赖;保存为 .html 文件即可打开。
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><meta name="color-scheme" content="light dark"><title>Toast · 轻反馈,不打断 — 交互演示</title><style>
:root{color-scheme:light;--bg:#f5f5f2;--paper:#fff;--ink:#242424;--muted:#85857f;--line:#e9e9e3;--soft:#f8f8f5;--accent:#df642b;--tint:#fff2e9;--shadow:0 12px 45px #2221;--overlay:#16161655;font-family:Arial,"PingFang SC","Microsoft YaHei",sans-serif}*{box-sizing:border-box}html,body{margin:0;min-width:0;min-height:100%;width:100%}body{background:var(--bg);color:var(--ink);font-size:12px;line-height:1.5}body[data-theme=dark]{color-scheme:dark;--bg:#171818;--paper:#222323;--ink:#ededeb;--muted:#aaa9a2;--line:#3b3b37;--soft:#292a28;--accent:#f39158;--tint:#413025;--shadow:0 12px 45px #0005;--overlay:#0009}button,input,textarea{font:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.45}button,input,textarea{outline:none}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button{border:0}button:active{transform:translateY(1px)}[hidden]{display:none!important}.stage{width:min(100%,680px);height:480px;margin:0 auto;padding:27px 30px 18px;position:relative;overflow:hidden;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:7px;font-size:10px;letter-spacing:1.5px;font-weight:700;color:var(--muted);margin-bottom:21px}.brand-mark{height:8px;width:8px;background:var(--accent)}.workspace{background:var(--paper);border:1px solid var(--line);border-radius:11px;box-shadow:0 3px 18px #22200008;padding:20px 20px 0;flex:1;min-height:0;display:flex;flex-direction:column}.workspace-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:19px}h1{font-size:21px;letter-spacing:-.8px;margin:0;font-weight:650}h2{font-size:18px;line-height:1.4;margin:10px 0 8px;font-weight:650;letter-spacing:-.4px}h3{font-size:12px;margin:20px 0 8px}p{margin:0}.subtitle{font-size:10px;color:var(--muted);margin-top:4px}.primary,.secondary{border-radius:5px;padding:8px 11px;white-space:nowrap;font-weight:600;font-size:11px;line-height:1.3}.primary{color:var(--paper);background:var(--ink)}.primary:hover{opacity:.85}.secondary{color:var(--ink);background:var(--paper);border:1px solid var(--line)}.secondary:hover{background:var(--soft)}.primary.accent-button{color:var(--accent);background:var(--tint);border:1px solid color-mix(in srgb,var(--accent) 15%,transparent)}.toolbar{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding:0 0 11px}.tab{font-size:10px;font-weight:600}.count{margin-left:6px;padding:1px 5px;border-radius:4px;background:var(--soft);color:var(--muted);font-size:9px}.toolbar-note{font-size:9px;color:var(--muted)}.task-list{padding:0;margin:0;list-style:none;overflow:auto;min-height:0;flex:1}.task{min-height:55px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:11px}.task:last-child{border-bottom:0}.check{width:14px;height:14px;flex:0 0 14px;border:1px solid #d6d6ce;border-radius:4px;font-size:10px;display:grid;place-items:center}.task.done .check{border-color:var(--accent);color:var(--accent);background:var(--tint)}.task.done .task-title{text-decoration:line-through;color:var(--muted)}.task-main{min-width:0;flex:1}.task-title{font-size:11px;font-weight:550;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.task-meta{font-size:9px;color:var(--muted);margin-top:3px}.priority{font-size:9px;padding:3px 5px;border-radius:4px;color:var(--accent);background:var(--tint);white-space:nowrap}.avatar{width:23px;height:23px;display:grid;place-items:center;flex-shrink:0;border-radius:50%;background:var(--soft);border:1px solid var(--line);color:var(--muted);font-size:9px}.workspace-foot{border-top:1px solid var(--line);color:var(--muted);padding:11px 0;font-size:9px;display:flex;align-items:center;gap:6px}.live-dot{width:4px;height:4px;border-radius:50%;background:var(--accent)}.demo-foot{display:flex;align-items:flex-start;gap:15px;justify-content:space-between;padding:15px 1px 0;min-height:53px}.demo-label{font-size:9px;color:var(--muted);letter-spacing:.6px;font-weight:600}.status{font-size:10px;color:var(--muted);margin-top:4px;min-height:15px}.text-button{color:var(--accent);background:none;padding:3px 4px;font-weight:600;font-size:11px}.reset{font-size:10px;color:var(--muted);border-bottom:1px solid var(--line);background:none;padding:3px 0;white-space:nowrap}.floating{position:fixed;z-index:10;background:var(--paper);border:1px solid var(--line);border-radius:9px;box-shadow:var(--shadow)}.toast{bottom:75px;right:max(22px,calc((100vw - 580px)/2));display:flex;align-items:center;gap:11px;padding:12px 15px;max-width:calc(100vw - 32px);animation:appear .18s ease-out;font-size:11px}.success-mark{color:var(--accent);font-size:15px}.hover-card{width:275px;padding:18px}.profile{display:flex;align-items:center;gap:10px;margin-bottom:13px}.profile strong{font-size:13px}.profile p{font-size:9px;color:var(--muted);margin-top:3px}.large{width:35px;height:35px;background:var(--tint);color:var(--accent);border:0;font-size:13px}.online{margin-left:auto;width:6px;height:6px;border-radius:50%;background:var(--accent)}.hover-card>p{color:var(--muted);font-size:10px}.profile-stats{display:flex;justify-content:space-between;border-top:1px solid var(--line);padding-top:12px;margin-top:15px;font-size:9px;color:var(--muted)}.profile-stats strong{display:block;color:var(--ink);font-size:16px;font-weight:500;margin-bottom:3px}.popconfirm{width:270px;padding:18px}.eyebrow{font-size:9px;font-weight:600;color:var(--accent);letter-spacing:1px}.popconfirm p,.center-dialog>p,.tour>p{font-size:11px;color:var(--muted);line-height:1.8}.button-row{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.panel-top{display:flex;justify-content:space-between;align-items:center;gap:10px}.icon-button{width:27px;height:27px;border-radius:5px;background:var(--soft);color:var(--muted);font-size:20px;line-height:1}.icon-button:hover{color:var(--ink)}dialog{color:var(--ink);background:var(--paper);border:1px solid var(--line);box-shadow:var(--shadow);padding:24px;max-width:calc(100vw - 32px);max-height:calc(100dvh - 32px)}dialog::backdrop{background:var(--overlay);backdrop-filter:blur(2px)}.drawer{position:fixed;inset:16px 16px 16px auto;margin:0;width:320px;height:calc(100dvh - 32px);border-radius:10px;animation:slide-in .2s ease-out;overflow:auto}.muted{color:var(--muted);font-size:11px}.detail-grid{display:grid;grid-template-columns:80px 1fr;gap:12px 8px;margin-top:25px;padding-bottom:20px;border-bottom:1px solid var(--line);font-size:10px}.detail-grid>span{color:var(--muted)}.detail-grid strong{font-weight:500}.body-copy{font-size:11px;line-height:1.9;color:var(--muted)}.subtasks{background:var(--soft);border-radius:5px;padding:10px;margin-top:14px;font-size:10px;color:var(--muted);line-height:2}.panel-actions{display:flex;justify-content:flex-end;margin-top:20px}.center-dialog{width:355px;border-radius:11px;animation:appear .18s ease-out}.form-dialog{width:360px;padding:20px}.form-dialog h2{margin:8px 0 16px}.form-dialog label{display:block;font-size:10px;font-weight:600;margin:12px 0 6px}.form-dialog .failure-option{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:9px;font-weight:400}.failure-option input{width:12px;height:12px;padding:0;accent-color:var(--accent)}.optional{font-weight:400;color:var(--muted);margin-left:5px}input,textarea{background:var(--paper);color:var(--ink);border:1px solid var(--line);border-radius:5px;width:100%;padding:9px 10px;font-size:11px;resize:vertical}input::placeholder,textarea::placeholder{color:var(--muted)}.accent{color:var(--accent)}.form-error{font-size:10px;color:var(--accent);margin-top:7px}.tooltip{z-index:20;position:fixed;width:max-content;max-width:calc(100vw - 32px);padding:8px 11px;border-radius:5px;background:var(--ink);color:var(--paper);font-size:10px;pointer-events:none;box-shadow:var(--shadow)}.tooltip-arrow{position:absolute;top:-4px;left:50%;width:8px;height:8px;transform:rotate(45deg);background:var(--ink)}.empty{text-align:center;color:var(--muted);padding:40px 10px;list-style:none;font-size:11px}.tour-blocker{position:fixed;inset:0;z-index:30}.tour-spotlight{position:fixed;z-index:31;border:2px solid var(--accent);border-radius:7px;box-shadow:0 0 0 9999px var(--overlay);pointer-events:none;transition:all .18s}.tour{z-index:32;width:275px;padding:16px}.tour-actions{display:flex;align-items:center;justify-content:space-between;margin-top:16px;gap:8px}.tour-actions .button-row{margin:0;gap:5px}.tour-actions .primary,.tour-actions .secondary{padding:7px 9px;font-size:10px}@keyframes appear{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@keyframes slide-in{from{opacity:.5;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@media(max-width:440px){.stage{padding:21px 16px 15px}.workspace{padding:17px 14px 0}.brand{margin-bottom:18px}.workspace-head{gap:7px}h1{font-size:19px}.workspace-head>.primary{font-size:10px;padding:8px}.toolbar-note{display:none}.task{gap:8px}.task-title{font-size:10px}.avatar{width:21px;height:21px}.priority{font-size:8px}.demo-foot{gap:8px}.status{font-size:9px}.drawer{width:calc(100vw - 32px)}.toast{right:16px}.tour{width:260px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
</style></head>
<body><main class="stage"><div class="brand"><span class="brand-mark"></span> SMALL STEPS / 任务工作台</div><section class="workspace" aria-label="每日任务工作台"><header class="workspace-head"><div data-tour="heading"><h1>今天,做好这几件事。</h1><p class="subtitle">把注意力留给真正重要的事情</p></div><button class="primary " data-testid="trigger">完成第一项</button></header><div class="toolbar" data-tour="toolbar"><span class="tab">全部任务<span class="count" data-testid="task-count">3</span></span><span class="toolbar-note">按优先级排列 ↕</span></div><ul class="task-list" data-testid="task-list" aria-label="任务列表"></ul><div class="workspace-foot"><span class="live-dot"></span> 每一步,都算数。<span style="margin-left:auto">今日计划 / 03</span></div></section><footer class="demo-foot"><div><div class="demo-label">01 / TOAST <span style="margin-left:7px;font-weight:400">轻反馈,不打断</span></div><p class="status" data-testid="status" role="status" aria-live="polite">完成一项任务,看看右下角。</p></div><button class="reset" data-testid="reset">↺ 重置</button></footer></main><div class="toast floating" data-testid="toast" role="status" hidden><span class="success-mark">✓</span><span>任务已完成</span><button class="text-button" data-testid="undo">撤销</button><button class="icon-button" data-testid="close" aria-label="关闭完成提示">×</button></div><script>(function sharedRuntime(kind, initialHint, runDemo) {
'use strict';
const $ = (id) => document.querySelector('[data-testid="' + id + '"]');
const trigger = $('trigger');
const status = $('status');
const list = $('task-list');
const base = [{ id: 1, title: '确认首页视觉稿', meta: '设计迭代 · 今天', owner: '林', priority: true }, { id: 2, title: '整理用户访谈记录', meta: '用户研究 · 明天', owner: '周' }, { id: 3, title: '准备周五的产品分享', meta: '团队协作 · 周五', owner: '陈' }];
let tasks = base.map((task) => ({ ...task }));
const timers = new Set();
const later = (callback, delay) => { const timer = setTimeout(() => { timers.delete(timer); callback(); }, delay); timers.add(timer); return timer; };
const stop = (timer) => { clearTimeout(timer); timers.delete(timer); };
const say = (text) => { status.textContent = text; };
const render = () => {
list.replaceChildren();
tasks.forEach((task) => {
const row = document.createElement('li'); row.className = 'task' + (task.done ? ' done' : ''); row.dataset.testid = 'task-row-' + task.id; row.dataset.taskId = String(task.id);
const check = document.createElement('span'); check.className = 'check'; check.textContent = task.done ? '✓' : ''; check.setAttribute('aria-hidden', 'true');
const main = document.createElement('div'); main.className = 'task-main';
const title = document.createElement('div'); title.className = 'task-title'; title.textContent = task.title;
const meta = document.createElement('div'); meta.className = 'task-meta'; meta.textContent = task.meta;
main.append(title, meta); row.append(check, main);
if (task.priority) { const badge = document.createElement('span'); badge.className = 'priority'; badge.textContent = '高优先级'; row.append(badge); }
const owner = document.createElement('span'); owner.className = 'avatar'; owner.textContent = task.owner; owner.setAttribute('aria-label', '负责人 ' + task.owner); row.append(owner); list.append(row);
});
if (!tasks.length) { const empty = document.createElement('li'); empty.className = 'empty'; empty.dataset.testid = 'empty'; empty.textContent = '任务都处理好了,留一点时间给自己。'; list.append(empty); }
$('task-count').textContent = String(tasks.length);
};
const place = (element, anchor = trigger, gap = 10) => {
const a = anchor.getBoundingClientRect(); const r = element.getBoundingClientRect();
element.style.left = Math.max(12, Math.min(a.right - r.width, innerWidth - r.width - 12)) + 'px';
element.style.top = Math.max(12, Math.min(a.bottom + gap, innerHeight - r.height - 12)) + 'px';
};
const openModal = (modal) => { if (!modal.open) modal.showModal(); trigger.setAttribute('aria-expanded', 'true'); };
const closeModal = (modal) => { if (modal.open) modal.close(); trigger.setAttribute('aria-expanded', 'false'); trigger.focus({ preventScroll: true }); };
const api = { $, trigger, list, later, stop, say, render, place, openModal, closeModal, getTasks: () => tasks, setTasks: (next) => { tasks = next; render(); } };
render();
const resetDemo = runDemo(api);
$('reset').addEventListener('click', () => { timers.forEach(clearTimeout); timers.clear(); resetDemo(); tasks = base.map((task) => ({ ...task })); render(); trigger.disabled = false; say(initialHint); $('reset').focus({ preventScroll: true }); });
document.body.dataset.theme = new URLSearchParams(location.search).get('theme') === 'dark' ? 'dark' : 'light';
document.body.dataset.ready = 'true';
document.body.dataset.demo = kind;
})("toast","完成一项任务,看看右下角。",function toastDemo({ $, trigger, later, stop, say, getTasks, setTasks }) {
const toast = $('toast'); let timer;
const hide = () => { stop(timer); toast.hidden = true; };
trigger.addEventListener('click', () => { hide(); setTasks(getTasks().map((task) => task.id === 1 ? { ...task, done: true } : task)); toast.hidden = false; say('第一项任务已完成,可在 2 秒内撤销。'); timer = later(() => { toast.hidden = true; say('完成状态已保留。'); }, 2000); });
$('undo').addEventListener('click', () => { hide(); setTasks(getTasks().map((task) => task.id === 1 ? { ...task, done: false } : task)); say('已撤销,任务恢复为待完成。'); trigger.focus(); });
$('close').addEventListener('click', () => { hide(); say('提示已关闭,完成状态已保留。'); trigger.focus(); });
return hide;
});</script></body></html>
2. Hover Card:先预览,再决定是否深入
适合在负责人名称旁预览角色和联系方式,避免为一点补充信息跳转整页。Radix Hover Card将它用于链接内容预览,并提示其内容会被屏幕阅读器忽略,因此必要信息还要有明确入口。
给 AI 的提示词:
给任务负责人增加信息预览。桌面悬停可查看,移入卡片不会立即关闭;点击和键盘也能查看同样信息,手机可轻点打开、关闭。不要把必须知道的信息只放在 hover 状态里。
**亲手验收:**从头像移入卡片,内容不应闪退;再用 Tab 和 Enter 操作一次。手机没有鼠标,若点按无法打开,就还没完成这项交互。
Hover Card · 悬停卡片
悬停或点击负责人,移入卡片,检查它是否仍然显示。
可直接操作。数据只在此演示区域临时变化,点击“重新开始”恢复初始状态。

查看完整可运行源码
与上方演示使用相同源码,无额外依赖;保存为 .html 文件即可打开。
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><meta name="color-scheme" content="light dark"><title>Hover Card · 信息,随意停留 — 交互演示</title><style>
:root{color-scheme:light;--bg:#f5f5f2;--paper:#fff;--ink:#242424;--muted:#85857f;--line:#e9e9e3;--soft:#f8f8f5;--accent:#df642b;--tint:#fff2e9;--shadow:0 12px 45px #2221;--overlay:#16161655;font-family:Arial,"PingFang SC","Microsoft YaHei",sans-serif}*{box-sizing:border-box}html,body{margin:0;min-width:0;min-height:100%;width:100%}body{background:var(--bg);color:var(--ink);font-size:12px;line-height:1.5}body[data-theme=dark]{color-scheme:dark;--bg:#171818;--paper:#222323;--ink:#ededeb;--muted:#aaa9a2;--line:#3b3b37;--soft:#292a28;--accent:#f39158;--tint:#413025;--shadow:0 12px 45px #0005;--overlay:#0009}button,input,textarea{font:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.45}button,input,textarea{outline:none}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button{border:0}button:active{transform:translateY(1px)}[hidden]{display:none!important}.stage{width:min(100%,680px);height:480px;margin:0 auto;padding:27px 30px 18px;position:relative;overflow:hidden;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:7px;font-size:10px;letter-spacing:1.5px;font-weight:700;color:var(--muted);margin-bottom:21px}.brand-mark{height:8px;width:8px;background:var(--accent)}.workspace{background:var(--paper);border:1px solid var(--line);border-radius:11px;box-shadow:0 3px 18px #22200008;padding:20px 20px 0;flex:1;min-height:0;display:flex;flex-direction:column}.workspace-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:19px}h1{font-size:21px;letter-spacing:-.8px;margin:0;font-weight:650}h2{font-size:18px;line-height:1.4;margin:10px 0 8px;font-weight:650;letter-spacing:-.4px}h3{font-size:12px;margin:20px 0 8px}p{margin:0}.subtitle{font-size:10px;color:var(--muted);margin-top:4px}.primary,.secondary{border-radius:5px;padding:8px 11px;white-space:nowrap;font-weight:600;font-size:11px;line-height:1.3}.primary{color:var(--paper);background:var(--ink)}.primary:hover{opacity:.85}.secondary{color:var(--ink);background:var(--paper);border:1px solid var(--line)}.secondary:hover{background:var(--soft)}.primary.accent-button{color:var(--accent);background:var(--tint);border:1px solid color-mix(in srgb,var(--accent) 15%,transparent)}.toolbar{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding:0 0 11px}.tab{font-size:10px;font-weight:600}.count{margin-left:6px;padding:1px 5px;border-radius:4px;background:var(--soft);color:var(--muted);font-size:9px}.toolbar-note{font-size:9px;color:var(--muted)}.task-list{padding:0;margin:0;list-style:none;overflow:auto;min-height:0;flex:1}.task{min-height:55px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:11px}.task:last-child{border-bottom:0}.check{width:14px;height:14px;flex:0 0 14px;border:1px solid #d6d6ce;border-radius:4px;font-size:10px;display:grid;place-items:center}.task.done .check{border-color:var(--accent);color:var(--accent);background:var(--tint)}.task.done .task-title{text-decoration:line-through;color:var(--muted)}.task-main{min-width:0;flex:1}.task-title{font-size:11px;font-weight:550;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.task-meta{font-size:9px;color:var(--muted);margin-top:3px}.priority{font-size:9px;padding:3px 5px;border-radius:4px;color:var(--accent);background:var(--tint);white-space:nowrap}.avatar{width:23px;height:23px;display:grid;place-items:center;flex-shrink:0;border-radius:50%;background:var(--soft);border:1px solid var(--line);color:var(--muted);font-size:9px}.workspace-foot{border-top:1px solid var(--line);color:var(--muted);padding:11px 0;font-size:9px;display:flex;align-items:center;gap:6px}.live-dot{width:4px;height:4px;border-radius:50%;background:var(--accent)}.demo-foot{display:flex;align-items:flex-start;gap:15px;justify-content:space-between;padding:15px 1px 0;min-height:53px}.demo-label{font-size:9px;color:var(--muted);letter-spacing:.6px;font-weight:600}.status{font-size:10px;color:var(--muted);margin-top:4px;min-height:15px}.text-button{color:var(--accent);background:none;padding:3px 4px;font-weight:600;font-size:11px}.reset{font-size:10px;color:var(--muted);border-bottom:1px solid var(--line);background:none;padding:3px 0;white-space:nowrap}.floating{position:fixed;z-index:10;background:var(--paper);border:1px solid var(--line);border-radius:9px;box-shadow:var(--shadow)}.toast{bottom:75px;right:max(22px,calc((100vw - 580px)/2));display:flex;align-items:center;gap:11px;padding:12px 15px;max-width:calc(100vw - 32px);animation:appear .18s ease-out;font-size:11px}.success-mark{color:var(--accent);font-size:15px}.hover-card{width:275px;padding:18px}.profile{display:flex;align-items:center;gap:10px;margin-bottom:13px}.profile strong{font-size:13px}.profile p{font-size:9px;color:var(--muted);margin-top:3px}.large{width:35px;height:35px;background:var(--tint);color:var(--accent);border:0;font-size:13px}.online{margin-left:auto;width:6px;height:6px;border-radius:50%;background:var(--accent)}.hover-card>p{color:var(--muted);font-size:10px}.profile-stats{display:flex;justify-content:space-between;border-top:1px solid var(--line);padding-top:12px;margin-top:15px;font-size:9px;color:var(--muted)}.profile-stats strong{display:block;color:var(--ink);font-size:16px;font-weight:500;margin-bottom:3px}.popconfirm{width:270px;padding:18px}.eyebrow{font-size:9px;font-weight:600;color:var(--accent);letter-spacing:1px}.popconfirm p,.center-dialog>p,.tour>p{font-size:11px;color:var(--muted);line-height:1.8}.button-row{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.panel-top{display:flex;justify-content:space-between;align-items:center;gap:10px}.icon-button{width:27px;height:27px;border-radius:5px;background:var(--soft);color:var(--muted);font-size:20px;line-height:1}.icon-button:hover{color:var(--ink)}dialog{color:var(--ink);background:var(--paper);border:1px solid var(--line);box-shadow:var(--shadow);padding:24px;max-width:calc(100vw - 32px);max-height:calc(100dvh - 32px)}dialog::backdrop{background:var(--overlay);backdrop-filter:blur(2px)}.drawer{position:fixed;inset:16px 16px 16px auto;margin:0;width:320px;height:calc(100dvh - 32px);border-radius:10px;animation:slide-in .2s ease-out;overflow:auto}.muted{color:var(--muted);font-size:11px}.detail-grid{display:grid;grid-template-columns:80px 1fr;gap:12px 8px;margin-top:25px;padding-bottom:20px;border-bottom:1px solid var(--line);font-size:10px}.detail-grid>span{color:var(--muted)}.detail-grid strong{font-weight:500}.body-copy{font-size:11px;line-height:1.9;color:var(--muted)}.subtasks{background:var(--soft);border-radius:5px;padding:10px;margin-top:14px;font-size:10px;color:var(--muted);line-height:2}.panel-actions{display:flex;justify-content:flex-end;margin-top:20px}.center-dialog{width:355px;border-radius:11px;animation:appear .18s ease-out}.form-dialog{width:360px;padding:20px}.form-dialog h2{margin:8px 0 16px}.form-dialog label{display:block;font-size:10px;font-weight:600;margin:12px 0 6px}.form-dialog .failure-option{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:9px;font-weight:400}.failure-option input{width:12px;height:12px;padding:0;accent-color:var(--accent)}.optional{font-weight:400;color:var(--muted);margin-left:5px}input,textarea{background:var(--paper);color:var(--ink);border:1px solid var(--line);border-radius:5px;width:100%;padding:9px 10px;font-size:11px;resize:vertical}input::placeholder,textarea::placeholder{color:var(--muted)}.accent{color:var(--accent)}.form-error{font-size:10px;color:var(--accent);margin-top:7px}.tooltip{z-index:20;position:fixed;width:max-content;max-width:calc(100vw - 32px);padding:8px 11px;border-radius:5px;background:var(--ink);color:var(--paper);font-size:10px;pointer-events:none;box-shadow:var(--shadow)}.tooltip-arrow{position:absolute;top:-4px;left:50%;width:8px;height:8px;transform:rotate(45deg);background:var(--ink)}.empty{text-align:center;color:var(--muted);padding:40px 10px;list-style:none;font-size:11px}.tour-blocker{position:fixed;inset:0;z-index:30}.tour-spotlight{position:fixed;z-index:31;border:2px solid var(--accent);border-radius:7px;box-shadow:0 0 0 9999px var(--overlay);pointer-events:none;transition:all .18s}.tour{z-index:32;width:275px;padding:16px}.tour-actions{display:flex;align-items:center;justify-content:space-between;margin-top:16px;gap:8px}.tour-actions .button-row{margin:0;gap:5px}.tour-actions .primary,.tour-actions .secondary{padding:7px 9px;font-size:10px}@keyframes appear{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@keyframes slide-in{from{opacity:.5;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@media(max-width:440px){.stage{padding:21px 16px 15px}.workspace{padding:17px 14px 0}.brand{margin-bottom:18px}.workspace-head{gap:7px}h1{font-size:19px}.workspace-head>.primary{font-size:10px;padding:8px}.toolbar-note{display:none}.task{gap:8px}.task-title{font-size:10px}.avatar{width:21px;height:21px}.priority{font-size:8px}.demo-foot{gap:8px}.status{font-size:9px}.drawer{width:calc(100vw - 32px)}.toast{right:16px}.tour{width:260px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
</style></head>
<body><main class="stage"><div class="brand"><span class="brand-mark"></span> SMALL STEPS / 任务工作台</div><section class="workspace" aria-label="每日任务工作台"><header class="workspace-head"><div data-tour="heading"><h1>今天,做好这几件事。</h1><p class="subtitle">把注意力留给真正重要的事情</p></div><button class="primary accent-button" data-testid="trigger">林小满 ↗</button></header><div class="toolbar" data-tour="toolbar"><span class="tab">全部任务<span class="count" data-testid="task-count">3</span></span><span class="toolbar-note">按优先级排列 ↕</span></div><ul class="task-list" data-testid="task-list" aria-label="任务列表"></ul><div class="workspace-foot"><span class="live-dot"></span> 每一步,都算数。<span style="margin-left:auto">今日计划 / 03</span></div></section><footer class="demo-foot"><div><div class="demo-label">02 / HOVER CARD <span style="margin-left:7px;font-weight:400">信息,随意停留</span></div><p class="status" data-testid="status" role="status" aria-live="polite">悬停或点击名字,了解任务负责人。</p></div><button class="reset" data-testid="reset">↺ 重置</button></footer></main><section class="hover-card floating" data-testid="hover-card" aria-label="林小满的资料" hidden><div class="profile"><span class="avatar large">林</span><div><strong>林小满</strong><p>产品设计师 · 在线</p></div><span class="online"></span></div><p>把复杂的问题,变成清楚的界面。</p><div class="profile-stats"><span><strong>12</strong>进行中</span><span><strong>86</strong>已完成</span><span><strong>98%</strong>准时率</span></div></section><script>(function sharedRuntime(kind, initialHint, runDemo) {
'use strict';
const $ = (id) => document.querySelector('[data-testid="' + id + '"]');
const trigger = $('trigger');
const status = $('status');
const list = $('task-list');
const base = [{ id: 1, title: '确认首页视觉稿', meta: '设计迭代 · 今天', owner: '林', priority: true }, { id: 2, title: '整理用户访谈记录', meta: '用户研究 · 明天', owner: '周' }, { id: 3, title: '准备周五的产品分享', meta: '团队协作 · 周五', owner: '陈' }];
let tasks = base.map((task) => ({ ...task }));
const timers = new Set();
const later = (callback, delay) => { const timer = setTimeout(() => { timers.delete(timer); callback(); }, delay); timers.add(timer); return timer; };
const stop = (timer) => { clearTimeout(timer); timers.delete(timer); };
const say = (text) => { status.textContent = text; };
const render = () => {
list.replaceChildren();
tasks.forEach((task) => {
const row = document.createElement('li'); row.className = 'task' + (task.done ? ' done' : ''); row.dataset.testid = 'task-row-' + task.id; row.dataset.taskId = String(task.id);
const check = document.createElement('span'); check.className = 'check'; check.textContent = task.done ? '✓' : ''; check.setAttribute('aria-hidden', 'true');
const main = document.createElement('div'); main.className = 'task-main';
const title = document.createElement('div'); title.className = 'task-title'; title.textContent = task.title;
const meta = document.createElement('div'); meta.className = 'task-meta'; meta.textContent = task.meta;
main.append(title, meta); row.append(check, main);
if (task.priority) { const badge = document.createElement('span'); badge.className = 'priority'; badge.textContent = '高优先级'; row.append(badge); }
const owner = document.createElement('span'); owner.className = 'avatar'; owner.textContent = task.owner; owner.setAttribute('aria-label', '负责人 ' + task.owner); row.append(owner); list.append(row);
});
if (!tasks.length) { const empty = document.createElement('li'); empty.className = 'empty'; empty.dataset.testid = 'empty'; empty.textContent = '任务都处理好了,留一点时间给自己。'; list.append(empty); }
$('task-count').textContent = String(tasks.length);
};
const place = (element, anchor = trigger, gap = 10) => {
const a = anchor.getBoundingClientRect(); const r = element.getBoundingClientRect();
element.style.left = Math.max(12, Math.min(a.right - r.width, innerWidth - r.width - 12)) + 'px';
element.style.top = Math.max(12, Math.min(a.bottom + gap, innerHeight - r.height - 12)) + 'px';
};
const openModal = (modal) => { if (!modal.open) modal.showModal(); trigger.setAttribute('aria-expanded', 'true'); };
const closeModal = (modal) => { if (modal.open) modal.close(); trigger.setAttribute('aria-expanded', 'false'); trigger.focus({ preventScroll: true }); };
const api = { $, trigger, list, later, stop, say, render, place, openModal, closeModal, getTasks: () => tasks, setTasks: (next) => { tasks = next; render(); } };
render();
const resetDemo = runDemo(api);
$('reset').addEventListener('click', () => { timers.forEach(clearTimeout); timers.clear(); resetDemo(); tasks = base.map((task) => ({ ...task })); render(); trigger.disabled = false; say(initialHint); $('reset').focus({ preventScroll: true }); });
document.body.dataset.theme = new URLSearchParams(location.search).get('theme') === 'dark' ? 'dark' : 'light';
document.body.dataset.ready = 'true';
document.body.dataset.demo = kind;
})("hover-card","悬停或点击名字,了解任务负责人。",function hoverDemo({ $, trigger, later, stop, place, say }) {
const card = $('hover-card'); let opening; let closing;
trigger.setAttribute('aria-expanded', 'false'); trigger.setAttribute('aria-controls', 'person-preview'); card.id = 'person-preview';
const cancel = () => { stop(opening); stop(closing); };
const show = () => { cancel(); card.hidden = false; trigger.setAttribute('aria-expanded', 'true'); place(card); say('林小满的个人资料已展开。'); };
const hide = () => { cancel(); card.hidden = true; trigger.setAttribute('aria-expanded', 'false'); };
const scheduleHide = () => { stop(opening); stop(closing); closing = later(() => { hide(); say('资料已收起,任务列表保持原样。'); }, 300); };
trigger.addEventListener('pointerenter', (event) => { if (event.pointerType !== 'touch') { cancel(); opening = later(show, 200); } });
trigger.addEventListener('pointerleave', scheduleHide);
trigger.addEventListener('focus', () => { cancel(); opening = later(show, 200); });
trigger.addEventListener('blur', scheduleHide);
trigger.addEventListener('click', () => { if (card.hidden) show(); else hide(); });
card.addEventListener('pointerenter', cancel); card.addEventListener('pointerleave', scheduleHide);
document.addEventListener('keydown', (event) => { if (event.key === 'Escape') hide(); });
document.addEventListener('pointerdown', (event) => { if (!card.contains(event.target) && !trigger.contains(event.target)) hide(); });
addEventListener('resize', () => { if (!card.hidden) place(card); });
return hide;
});</script></body></html>
3. Drawer:留住列表背景,展开任务详情
抽屉从侧边进入,适合内容较长的任务详情。它让用户记得自己从哪条任务进入,但看得到背景,不代表背景一定可以操作。本例采用模态抽屉。
给 AI 的提示词:
点击任务详情后打开右侧 Drawer,展示任务信息和关闭按钮。背景仍可见,但本例打开期间禁止背景交互;键盘焦点留在抽屉内,Esc 可关闭,关闭后返回触发按钮。窄屏下限制宽度,长内容在抽屉内滚动。
**亲手验收:**打开后尝试点击背景、连续按 Tab,再关闭检查焦点。手机上同时检查横向溢出、底部内容与关闭按钮是否可达。
Drawer · 抽屉
打开任务详情,滚动面板,再关闭回到列表。
可直接操作。数据只在此演示区域临时变化,点击“重新开始”恢复初始状态。

查看完整可运行源码
与上方演示使用相同源码,无额外依赖;保存为 .html 文件即可打开。
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><meta name="color-scheme" content="light dark"><title>Drawer · 详情,从侧边展开 — 交互演示</title><style>
:root{color-scheme:light;--bg:#f5f5f2;--paper:#fff;--ink:#242424;--muted:#85857f;--line:#e9e9e3;--soft:#f8f8f5;--accent:#df642b;--tint:#fff2e9;--shadow:0 12px 45px #2221;--overlay:#16161655;font-family:Arial,"PingFang SC","Microsoft YaHei",sans-serif}*{box-sizing:border-box}html,body{margin:0;min-width:0;min-height:100%;width:100%}body{background:var(--bg);color:var(--ink);font-size:12px;line-height:1.5}body[data-theme=dark]{color-scheme:dark;--bg:#171818;--paper:#222323;--ink:#ededeb;--muted:#aaa9a2;--line:#3b3b37;--soft:#292a28;--accent:#f39158;--tint:#413025;--shadow:0 12px 45px #0005;--overlay:#0009}button,input,textarea{font:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.45}button,input,textarea{outline:none}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button{border:0}button:active{transform:translateY(1px)}[hidden]{display:none!important}.stage{width:min(100%,680px);height:480px;margin:0 auto;padding:27px 30px 18px;position:relative;overflow:hidden;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:7px;font-size:10px;letter-spacing:1.5px;font-weight:700;color:var(--muted);margin-bottom:21px}.brand-mark{height:8px;width:8px;background:var(--accent)}.workspace{background:var(--paper);border:1px solid var(--line);border-radius:11px;box-shadow:0 3px 18px #22200008;padding:20px 20px 0;flex:1;min-height:0;display:flex;flex-direction:column}.workspace-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:19px}h1{font-size:21px;letter-spacing:-.8px;margin:0;font-weight:650}h2{font-size:18px;line-height:1.4;margin:10px 0 8px;font-weight:650;letter-spacing:-.4px}h3{font-size:12px;margin:20px 0 8px}p{margin:0}.subtitle{font-size:10px;color:var(--muted);margin-top:4px}.primary,.secondary{border-radius:5px;padding:8px 11px;white-space:nowrap;font-weight:600;font-size:11px;line-height:1.3}.primary{color:var(--paper);background:var(--ink)}.primary:hover{opacity:.85}.secondary{color:var(--ink);background:var(--paper);border:1px solid var(--line)}.secondary:hover{background:var(--soft)}.primary.accent-button{color:var(--accent);background:var(--tint);border:1px solid color-mix(in srgb,var(--accent) 15%,transparent)}.toolbar{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding:0 0 11px}.tab{font-size:10px;font-weight:600}.count{margin-left:6px;padding:1px 5px;border-radius:4px;background:var(--soft);color:var(--muted);font-size:9px}.toolbar-note{font-size:9px;color:var(--muted)}.task-list{padding:0;margin:0;list-style:none;overflow:auto;min-height:0;flex:1}.task{min-height:55px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:11px}.task:last-child{border-bottom:0}.check{width:14px;height:14px;flex:0 0 14px;border:1px solid #d6d6ce;border-radius:4px;font-size:10px;display:grid;place-items:center}.task.done .check{border-color:var(--accent);color:var(--accent);background:var(--tint)}.task.done .task-title{text-decoration:line-through;color:var(--muted)}.task-main{min-width:0;flex:1}.task-title{font-size:11px;font-weight:550;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.task-meta{font-size:9px;color:var(--muted);margin-top:3px}.priority{font-size:9px;padding:3px 5px;border-radius:4px;color:var(--accent);background:var(--tint);white-space:nowrap}.avatar{width:23px;height:23px;display:grid;place-items:center;flex-shrink:0;border-radius:50%;background:var(--soft);border:1px solid var(--line);color:var(--muted);font-size:9px}.workspace-foot{border-top:1px solid var(--line);color:var(--muted);padding:11px 0;font-size:9px;display:flex;align-items:center;gap:6px}.live-dot{width:4px;height:4px;border-radius:50%;background:var(--accent)}.demo-foot{display:flex;align-items:flex-start;gap:15px;justify-content:space-between;padding:15px 1px 0;min-height:53px}.demo-label{font-size:9px;color:var(--muted);letter-spacing:.6px;font-weight:600}.status{font-size:10px;color:var(--muted);margin-top:4px;min-height:15px}.text-button{color:var(--accent);background:none;padding:3px 4px;font-weight:600;font-size:11px}.reset{font-size:10px;color:var(--muted);border-bottom:1px solid var(--line);background:none;padding:3px 0;white-space:nowrap}.floating{position:fixed;z-index:10;background:var(--paper);border:1px solid var(--line);border-radius:9px;box-shadow:var(--shadow)}.toast{bottom:75px;right:max(22px,calc((100vw - 580px)/2));display:flex;align-items:center;gap:11px;padding:12px 15px;max-width:calc(100vw - 32px);animation:appear .18s ease-out;font-size:11px}.success-mark{color:var(--accent);font-size:15px}.hover-card{width:275px;padding:18px}.profile{display:flex;align-items:center;gap:10px;margin-bottom:13px}.profile strong{font-size:13px}.profile p{font-size:9px;color:var(--muted);margin-top:3px}.large{width:35px;height:35px;background:var(--tint);color:var(--accent);border:0;font-size:13px}.online{margin-left:auto;width:6px;height:6px;border-radius:50%;background:var(--accent)}.hover-card>p{color:var(--muted);font-size:10px}.profile-stats{display:flex;justify-content:space-between;border-top:1px solid var(--line);padding-top:12px;margin-top:15px;font-size:9px;color:var(--muted)}.profile-stats strong{display:block;color:var(--ink);font-size:16px;font-weight:500;margin-bottom:3px}.popconfirm{width:270px;padding:18px}.eyebrow{font-size:9px;font-weight:600;color:var(--accent);letter-spacing:1px}.popconfirm p,.center-dialog>p,.tour>p{font-size:11px;color:var(--muted);line-height:1.8}.button-row{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.panel-top{display:flex;justify-content:space-between;align-items:center;gap:10px}.icon-button{width:27px;height:27px;border-radius:5px;background:var(--soft);color:var(--muted);font-size:20px;line-height:1}.icon-button:hover{color:var(--ink)}dialog{color:var(--ink);background:var(--paper);border:1px solid var(--line);box-shadow:var(--shadow);padding:24px;max-width:calc(100vw - 32px);max-height:calc(100dvh - 32px)}dialog::backdrop{background:var(--overlay);backdrop-filter:blur(2px)}.drawer{position:fixed;inset:16px 16px 16px auto;margin:0;width:320px;height:calc(100dvh - 32px);border-radius:10px;animation:slide-in .2s ease-out;overflow:auto}.muted{color:var(--muted);font-size:11px}.detail-grid{display:grid;grid-template-columns:80px 1fr;gap:12px 8px;margin-top:25px;padding-bottom:20px;border-bottom:1px solid var(--line);font-size:10px}.detail-grid>span{color:var(--muted)}.detail-grid strong{font-weight:500}.body-copy{font-size:11px;line-height:1.9;color:var(--muted)}.subtasks{background:var(--soft);border-radius:5px;padding:10px;margin-top:14px;font-size:10px;color:var(--muted);line-height:2}.panel-actions{display:flex;justify-content:flex-end;margin-top:20px}.center-dialog{width:355px;border-radius:11px;animation:appear .18s ease-out}.form-dialog{width:360px;padding:20px}.form-dialog h2{margin:8px 0 16px}.form-dialog label{display:block;font-size:10px;font-weight:600;margin:12px 0 6px}.form-dialog .failure-option{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:9px;font-weight:400}.failure-option input{width:12px;height:12px;padding:0;accent-color:var(--accent)}.optional{font-weight:400;color:var(--muted);margin-left:5px}input,textarea{background:var(--paper);color:var(--ink);border:1px solid var(--line);border-radius:5px;width:100%;padding:9px 10px;font-size:11px;resize:vertical}input::placeholder,textarea::placeholder{color:var(--muted)}.accent{color:var(--accent)}.form-error{font-size:10px;color:var(--accent);margin-top:7px}.tooltip{z-index:20;position:fixed;width:max-content;max-width:calc(100vw - 32px);padding:8px 11px;border-radius:5px;background:var(--ink);color:var(--paper);font-size:10px;pointer-events:none;box-shadow:var(--shadow)}.tooltip-arrow{position:absolute;top:-4px;left:50%;width:8px;height:8px;transform:rotate(45deg);background:var(--ink)}.empty{text-align:center;color:var(--muted);padding:40px 10px;list-style:none;font-size:11px}.tour-blocker{position:fixed;inset:0;z-index:30}.tour-spotlight{position:fixed;z-index:31;border:2px solid var(--accent);border-radius:7px;box-shadow:0 0 0 9999px var(--overlay);pointer-events:none;transition:all .18s}.tour{z-index:32;width:275px;padding:16px}.tour-actions{display:flex;align-items:center;justify-content:space-between;margin-top:16px;gap:8px}.tour-actions .button-row{margin:0;gap:5px}.tour-actions .primary,.tour-actions .secondary{padding:7px 9px;font-size:10px}@keyframes appear{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@keyframes slide-in{from{opacity:.5;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@media(max-width:440px){.stage{padding:21px 16px 15px}.workspace{padding:17px 14px 0}.brand{margin-bottom:18px}.workspace-head{gap:7px}h1{font-size:19px}.workspace-head>.primary{font-size:10px;padding:8px}.toolbar-note{display:none}.task{gap:8px}.task-title{font-size:10px}.avatar{width:21px;height:21px}.priority{font-size:8px}.demo-foot{gap:8px}.status{font-size:9px}.drawer{width:calc(100vw - 32px)}.toast{right:16px}.tour{width:260px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
</style></head>
<body><main class="stage"><div class="brand"><span class="brand-mark"></span> SMALL STEPS / 任务工作台</div><section class="workspace" aria-label="每日任务工作台"><header class="workspace-head"><div data-tour="heading"><h1>今天,做好这几件事。</h1><p class="subtitle">把注意力留给真正重要的事情</p></div><button class="primary " data-testid="trigger">查看任务</button></header><div class="toolbar" data-tour="toolbar"><span class="tab">全部任务<span class="count" data-testid="task-count">3</span></span><span class="toolbar-note">按优先级排列 ↕</span></div><ul class="task-list" data-testid="task-list" aria-label="任务列表"></ul><div class="workspace-foot"><span class="live-dot"></span> 每一步,都算数。<span style="margin-left:auto">今日计划 / 03</span></div></section><footer class="demo-foot"><div><div class="demo-label">03 / DRAWER <span style="margin-left:7px;font-weight:400">详情,从侧边展开</span></div><p class="status" data-testid="status" role="status" aria-live="polite">保留列表上下文,在侧边查看任务详情。</p></div><button class="reset" data-testid="reset">↺ 重置</button></footer></main><dialog class="drawer" data-testid="drawer" aria-labelledby="drawer-title"><div class="panel-top"><span class="eyebrow">TASK / 001</span><button class="icon-button" data-testid="close" aria-label="关闭详情">×</button></div><h2 id="drawer-title">确认首页视觉稿</h2><p class="muted">把最后一点细节,做得刚刚好。</p><div class="detail-grid"><span>负责人</span><strong>林小满</strong><span>截止日期</span><strong>今天 18:00</strong><span>优先级</span><strong><span class="priority">高优先级</span></strong></div><h3>任务说明</h3><p class="body-copy">检查首页首屏的布局、文字层级与移动端留白,整理反馈后交付开发。</p><div class="subtasks"><p>✓ 检查标题与正文层级</p><p>✓ 确认按钮与交互状态</p><p>○ 复核移动端适配</p></div><div class="panel-actions"><button class="secondary" data-testid="cancel">返回列表</button></div></dialog><script>(function sharedRuntime(kind, initialHint, runDemo) {
'use strict';
const $ = (id) => document.querySelector('[data-testid="' + id + '"]');
const trigger = $('trigger');
const status = $('status');
const list = $('task-list');
const base = [{ id: 1, title: '确认首页视觉稿', meta: '设计迭代 · 今天', owner: '林', priority: true }, { id: 2, title: '整理用户访谈记录', meta: '用户研究 · 明天', owner: '周' }, { id: 3, title: '准备周五的产品分享', meta: '团队协作 · 周五', owner: '陈' }];
let tasks = base.map((task) => ({ ...task }));
const timers = new Set();
const later = (callback, delay) => { const timer = setTimeout(() => { timers.delete(timer); callback(); }, delay); timers.add(timer); return timer; };
const stop = (timer) => { clearTimeout(timer); timers.delete(timer); };
const say = (text) => { status.textContent = text; };
const render = () => {
list.replaceChildren();
tasks.forEach((task) => {
const row = document.createElement('li'); row.className = 'task' + (task.done ? ' done' : ''); row.dataset.testid = 'task-row-' + task.id; row.dataset.taskId = String(task.id);
const check = document.createElement('span'); check.className = 'check'; check.textContent = task.done ? '✓' : ''; check.setAttribute('aria-hidden', 'true');
const main = document.createElement('div'); main.className = 'task-main';
const title = document.createElement('div'); title.className = 'task-title'; title.textContent = task.title;
const meta = document.createElement('div'); meta.className = 'task-meta'; meta.textContent = task.meta;
main.append(title, meta); row.append(check, main);
if (task.priority) { const badge = document.createElement('span'); badge.className = 'priority'; badge.textContent = '高优先级'; row.append(badge); }
const owner = document.createElement('span'); owner.className = 'avatar'; owner.textContent = task.owner; owner.setAttribute('aria-label', '负责人 ' + task.owner); row.append(owner); list.append(row);
});
if (!tasks.length) { const empty = document.createElement('li'); empty.className = 'empty'; empty.dataset.testid = 'empty'; empty.textContent = '任务都处理好了,留一点时间给自己。'; list.append(empty); }
$('task-count').textContent = String(tasks.length);
};
const place = (element, anchor = trigger, gap = 10) => {
const a = anchor.getBoundingClientRect(); const r = element.getBoundingClientRect();
element.style.left = Math.max(12, Math.min(a.right - r.width, innerWidth - r.width - 12)) + 'px';
element.style.top = Math.max(12, Math.min(a.bottom + gap, innerHeight - r.height - 12)) + 'px';
};
const openModal = (modal) => { if (!modal.open) modal.showModal(); trigger.setAttribute('aria-expanded', 'true'); };
const closeModal = (modal) => { if (modal.open) modal.close(); trigger.setAttribute('aria-expanded', 'false'); trigger.focus({ preventScroll: true }); };
const api = { $, trigger, list, later, stop, say, render, place, openModal, closeModal, getTasks: () => tasks, setTasks: (next) => { tasks = next; render(); } };
render();
const resetDemo = runDemo(api);
$('reset').addEventListener('click', () => { timers.forEach(clearTimeout); timers.clear(); resetDemo(); tasks = base.map((task) => ({ ...task })); render(); trigger.disabled = false; say(initialHint); $('reset').focus({ preventScroll: true }); });
document.body.dataset.theme = new URLSearchParams(location.search).get('theme') === 'dark' ? 'dark' : 'light';
document.body.dataset.ready = 'true';
document.body.dataset.demo = kind;
})("drawer","保留列表上下文,在侧边查看任务详情。",function drawerDemo({ $, trigger, say, openModal, closeModal }) {
const drawer = $('drawer'); trigger.setAttribute('aria-haspopup', 'dialog');
trigger.addEventListener('click', () => { openModal(drawer); $('close').focus(); say('任务详情已展开,按 Esc 可返回。'); });
const close = () => { closeModal(drawer); say('已返回任务列表。'); };
$('close').addEventListener('click', close); $('cancel').addEventListener('click', close);
drawer.addEventListener('cancel', (event) => { event.preventDefault(); close(); });
return () => closeModal(drawer);
});</script></body></html>
4. Popconfirm:在操作旁边多确认一步
适合单条任务的删除等简短确认:用户仍能看清正在处理哪一项,无需阅读整页说明。若风险很高、后果复杂,应升级为下一类警告确认。
给 AI 的提示词:
在单条任务的删除按钮旁显示 Popconfirm,写清要删除的对象,提供取消和确认删除。首次点击只打开确认;取消、Esc 或关闭浮层均不得删除。确认成功后再更新列表,失败则保留任务并显示错误。
**亲手验收:**先取消,再重新打开确认,检查两条路径的结果。手机上确认框要留在屏幕内,两个按钮不能挤在一起造成误触。下面仅演示内存中的删除。
Popconfirm · 气泡确认
删除一条任务,先取消,再确认并撤销。
可直接操作。数据只在此演示区域临时变化,点击“重新开始”恢复初始状态。

查看完整可运行源码
与上方演示使用相同源码,无额外依赖;保存为 .html 文件即可打开。
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><meta name="color-scheme" content="light dark"><title>Popconfirm · 就地确认,再行动 — 交互演示</title><style>
:root{color-scheme:light;--bg:#f5f5f2;--paper:#fff;--ink:#242424;--muted:#85857f;--line:#e9e9e3;--soft:#f8f8f5;--accent:#df642b;--tint:#fff2e9;--shadow:0 12px 45px #2221;--overlay:#16161655;font-family:Arial,"PingFang SC","Microsoft YaHei",sans-serif}*{box-sizing:border-box}html,body{margin:0;min-width:0;min-height:100%;width:100%}body{background:var(--bg);color:var(--ink);font-size:12px;line-height:1.5}body[data-theme=dark]{color-scheme:dark;--bg:#171818;--paper:#222323;--ink:#ededeb;--muted:#aaa9a2;--line:#3b3b37;--soft:#292a28;--accent:#f39158;--tint:#413025;--shadow:0 12px 45px #0005;--overlay:#0009}button,input,textarea{font:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.45}button,input,textarea{outline:none}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button{border:0}button:active{transform:translateY(1px)}[hidden]{display:none!important}.stage{width:min(100%,680px);height:480px;margin:0 auto;padding:27px 30px 18px;position:relative;overflow:hidden;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:7px;font-size:10px;letter-spacing:1.5px;font-weight:700;color:var(--muted);margin-bottom:21px}.brand-mark{height:8px;width:8px;background:var(--accent)}.workspace{background:var(--paper);border:1px solid var(--line);border-radius:11px;box-shadow:0 3px 18px #22200008;padding:20px 20px 0;flex:1;min-height:0;display:flex;flex-direction:column}.workspace-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:19px}h1{font-size:21px;letter-spacing:-.8px;margin:0;font-weight:650}h2{font-size:18px;line-height:1.4;margin:10px 0 8px;font-weight:650;letter-spacing:-.4px}h3{font-size:12px;margin:20px 0 8px}p{margin:0}.subtitle{font-size:10px;color:var(--muted);margin-top:4px}.primary,.secondary{border-radius:5px;padding:8px 11px;white-space:nowrap;font-weight:600;font-size:11px;line-height:1.3}.primary{color:var(--paper);background:var(--ink)}.primary:hover{opacity:.85}.secondary{color:var(--ink);background:var(--paper);border:1px solid var(--line)}.secondary:hover{background:var(--soft)}.primary.accent-button{color:var(--accent);background:var(--tint);border:1px solid color-mix(in srgb,var(--accent) 15%,transparent)}.toolbar{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding:0 0 11px}.tab{font-size:10px;font-weight:600}.count{margin-left:6px;padding:1px 5px;border-radius:4px;background:var(--soft);color:var(--muted);font-size:9px}.toolbar-note{font-size:9px;color:var(--muted)}.task-list{padding:0;margin:0;list-style:none;overflow:auto;min-height:0;flex:1}.task{min-height:55px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:11px}.task:last-child{border-bottom:0}.check{width:14px;height:14px;flex:0 0 14px;border:1px solid #d6d6ce;border-radius:4px;font-size:10px;display:grid;place-items:center}.task.done .check{border-color:var(--accent);color:var(--accent);background:var(--tint)}.task.done .task-title{text-decoration:line-through;color:var(--muted)}.task-main{min-width:0;flex:1}.task-title{font-size:11px;font-weight:550;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.task-meta{font-size:9px;color:var(--muted);margin-top:3px}.priority{font-size:9px;padding:3px 5px;border-radius:4px;color:var(--accent);background:var(--tint);white-space:nowrap}.avatar{width:23px;height:23px;display:grid;place-items:center;flex-shrink:0;border-radius:50%;background:var(--soft);border:1px solid var(--line);color:var(--muted);font-size:9px}.workspace-foot{border-top:1px solid var(--line);color:var(--muted);padding:11px 0;font-size:9px;display:flex;align-items:center;gap:6px}.live-dot{width:4px;height:4px;border-radius:50%;background:var(--accent)}.demo-foot{display:flex;align-items:flex-start;gap:15px;justify-content:space-between;padding:15px 1px 0;min-height:53px}.demo-label{font-size:9px;color:var(--muted);letter-spacing:.6px;font-weight:600}.status{font-size:10px;color:var(--muted);margin-top:4px;min-height:15px}.text-button{color:var(--accent);background:none;padding:3px 4px;font-weight:600;font-size:11px}.reset{font-size:10px;color:var(--muted);border-bottom:1px solid var(--line);background:none;padding:3px 0;white-space:nowrap}.floating{position:fixed;z-index:10;background:var(--paper);border:1px solid var(--line);border-radius:9px;box-shadow:var(--shadow)}.toast{bottom:75px;right:max(22px,calc((100vw - 580px)/2));display:flex;align-items:center;gap:11px;padding:12px 15px;max-width:calc(100vw - 32px);animation:appear .18s ease-out;font-size:11px}.success-mark{color:var(--accent);font-size:15px}.hover-card{width:275px;padding:18px}.profile{display:flex;align-items:center;gap:10px;margin-bottom:13px}.profile strong{font-size:13px}.profile p{font-size:9px;color:var(--muted);margin-top:3px}.large{width:35px;height:35px;background:var(--tint);color:var(--accent);border:0;font-size:13px}.online{margin-left:auto;width:6px;height:6px;border-radius:50%;background:var(--accent)}.hover-card>p{color:var(--muted);font-size:10px}.profile-stats{display:flex;justify-content:space-between;border-top:1px solid var(--line);padding-top:12px;margin-top:15px;font-size:9px;color:var(--muted)}.profile-stats strong{display:block;color:var(--ink);font-size:16px;font-weight:500;margin-bottom:3px}.popconfirm{width:270px;padding:18px}.eyebrow{font-size:9px;font-weight:600;color:var(--accent);letter-spacing:1px}.popconfirm p,.center-dialog>p,.tour>p{font-size:11px;color:var(--muted);line-height:1.8}.button-row{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.panel-top{display:flex;justify-content:space-between;align-items:center;gap:10px}.icon-button{width:27px;height:27px;border-radius:5px;background:var(--soft);color:var(--muted);font-size:20px;line-height:1}.icon-button:hover{color:var(--ink)}dialog{color:var(--ink);background:var(--paper);border:1px solid var(--line);box-shadow:var(--shadow);padding:24px;max-width:calc(100vw - 32px);max-height:calc(100dvh - 32px)}dialog::backdrop{background:var(--overlay);backdrop-filter:blur(2px)}.drawer{position:fixed;inset:16px 16px 16px auto;margin:0;width:320px;height:calc(100dvh - 32px);border-radius:10px;animation:slide-in .2s ease-out;overflow:auto}.muted{color:var(--muted);font-size:11px}.detail-grid{display:grid;grid-template-columns:80px 1fr;gap:12px 8px;margin-top:25px;padding-bottom:20px;border-bottom:1px solid var(--line);font-size:10px}.detail-grid>span{color:var(--muted)}.detail-grid strong{font-weight:500}.body-copy{font-size:11px;line-height:1.9;color:var(--muted)}.subtasks{background:var(--soft);border-radius:5px;padding:10px;margin-top:14px;font-size:10px;color:var(--muted);line-height:2}.panel-actions{display:flex;justify-content:flex-end;margin-top:20px}.center-dialog{width:355px;border-radius:11px;animation:appear .18s ease-out}.form-dialog{width:360px;padding:20px}.form-dialog h2{margin:8px 0 16px}.form-dialog label{display:block;font-size:10px;font-weight:600;margin:12px 0 6px}.form-dialog .failure-option{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:9px;font-weight:400}.failure-option input{width:12px;height:12px;padding:0;accent-color:var(--accent)}.optional{font-weight:400;color:var(--muted);margin-left:5px}input,textarea{background:var(--paper);color:var(--ink);border:1px solid var(--line);border-radius:5px;width:100%;padding:9px 10px;font-size:11px;resize:vertical}input::placeholder,textarea::placeholder{color:var(--muted)}.accent{color:var(--accent)}.form-error{font-size:10px;color:var(--accent);margin-top:7px}.tooltip{z-index:20;position:fixed;width:max-content;max-width:calc(100vw - 32px);padding:8px 11px;border-radius:5px;background:var(--ink);color:var(--paper);font-size:10px;pointer-events:none;box-shadow:var(--shadow)}.tooltip-arrow{position:absolute;top:-4px;left:50%;width:8px;height:8px;transform:rotate(45deg);background:var(--ink)}.empty{text-align:center;color:var(--muted);padding:40px 10px;list-style:none;font-size:11px}.tour-blocker{position:fixed;inset:0;z-index:30}.tour-spotlight{position:fixed;z-index:31;border:2px solid var(--accent);border-radius:7px;box-shadow:0 0 0 9999px var(--overlay);pointer-events:none;transition:all .18s}.tour{z-index:32;width:275px;padding:16px}.tour-actions{display:flex;align-items:center;justify-content:space-between;margin-top:16px;gap:8px}.tour-actions .button-row{margin:0;gap:5px}.tour-actions .primary,.tour-actions .secondary{padding:7px 9px;font-size:10px}@keyframes appear{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@keyframes slide-in{from{opacity:.5;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@media(max-width:440px){.stage{padding:21px 16px 15px}.workspace{padding:17px 14px 0}.brand{margin-bottom:18px}.workspace-head{gap:7px}h1{font-size:19px}.workspace-head>.primary{font-size:10px;padding:8px}.toolbar-note{display:none}.task{gap:8px}.task-title{font-size:10px}.avatar{width:21px;height:21px}.priority{font-size:8px}.demo-foot{gap:8px}.status{font-size:9px}.drawer{width:calc(100vw - 32px)}.toast{right:16px}.tour{width:260px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
</style></head>
<body><main class="stage"><div class="brand"><span class="brand-mark"></span> SMALL STEPS / 任务工作台</div><section class="workspace" aria-label="每日任务工作台"><header class="workspace-head"><div data-tour="heading"><h1>今天,做好这几件事。</h1><p class="subtitle">把注意力留给真正重要的事情</p></div><button class="primary " data-testid="trigger">删除第一项</button></header><div class="toolbar" data-tour="toolbar"><span class="tab">全部任务<span class="count" data-testid="task-count">3</span></span><span class="toolbar-note">按优先级排列 ↕</span></div><ul class="task-list" data-testid="task-list" aria-label="任务列表"></ul><div class="workspace-foot"><span class="live-dot"></span> 每一步,都算数。<span style="margin-left:auto">今日计划 / 03</span></div></section><footer class="demo-foot"><div><div class="demo-label">04 / POPCONFIRM <span style="margin-left:7px;font-weight:400">就地确认,再行动</span></div><p class="status" data-testid="status" role="status" aria-live="polite">删除前确认一次;删除后仍可撤销。</p></div><button class="reset" data-testid="reset">↺ 重置</button></footer></main><section class="popconfirm floating" data-testid="popconfirm" role="alertdialog" aria-modal="false" aria-labelledby="pop-title" hidden><span class="eyebrow">请确认操作</span><h2 id="pop-title">删除这项任务?</h2><p>“确认首页视觉稿”将从列表中移除。</p><div class="button-row"><button class="secondary" data-testid="cancel">保留任务</button><button class="primary" data-testid="confirm">确认删除</button></div></section><div class="toast floating" data-testid="toast" role="status" hidden><span>任务已删除</span><button class="text-button" data-testid="undo">撤销</button></div><script>(function sharedRuntime(kind, initialHint, runDemo) {
'use strict';
const $ = (id) => document.querySelector('[data-testid="' + id + '"]');
const trigger = $('trigger');
const status = $('status');
const list = $('task-list');
const base = [{ id: 1, title: '确认首页视觉稿', meta: '设计迭代 · 今天', owner: '林', priority: true }, { id: 2, title: '整理用户访谈记录', meta: '用户研究 · 明天', owner: '周' }, { id: 3, title: '准备周五的产品分享', meta: '团队协作 · 周五', owner: '陈' }];
let tasks = base.map((task) => ({ ...task }));
const timers = new Set();
const later = (callback, delay) => { const timer = setTimeout(() => { timers.delete(timer); callback(); }, delay); timers.add(timer); return timer; };
const stop = (timer) => { clearTimeout(timer); timers.delete(timer); };
const say = (text) => { status.textContent = text; };
const render = () => {
list.replaceChildren();
tasks.forEach((task) => {
const row = document.createElement('li'); row.className = 'task' + (task.done ? ' done' : ''); row.dataset.testid = 'task-row-' + task.id; row.dataset.taskId = String(task.id);
const check = document.createElement('span'); check.className = 'check'; check.textContent = task.done ? '✓' : ''; check.setAttribute('aria-hidden', 'true');
const main = document.createElement('div'); main.className = 'task-main';
const title = document.createElement('div'); title.className = 'task-title'; title.textContent = task.title;
const meta = document.createElement('div'); meta.className = 'task-meta'; meta.textContent = task.meta;
main.append(title, meta); row.append(check, main);
if (task.priority) { const badge = document.createElement('span'); badge.className = 'priority'; badge.textContent = '高优先级'; row.append(badge); }
const owner = document.createElement('span'); owner.className = 'avatar'; owner.textContent = task.owner; owner.setAttribute('aria-label', '负责人 ' + task.owner); row.append(owner); list.append(row);
});
if (!tasks.length) { const empty = document.createElement('li'); empty.className = 'empty'; empty.dataset.testid = 'empty'; empty.textContent = '任务都处理好了,留一点时间给自己。'; list.append(empty); }
$('task-count').textContent = String(tasks.length);
};
const place = (element, anchor = trigger, gap = 10) => {
const a = anchor.getBoundingClientRect(); const r = element.getBoundingClientRect();
element.style.left = Math.max(12, Math.min(a.right - r.width, innerWidth - r.width - 12)) + 'px';
element.style.top = Math.max(12, Math.min(a.bottom + gap, innerHeight - r.height - 12)) + 'px';
};
const openModal = (modal) => { if (!modal.open) modal.showModal(); trigger.setAttribute('aria-expanded', 'true'); };
const closeModal = (modal) => { if (modal.open) modal.close(); trigger.setAttribute('aria-expanded', 'false'); trigger.focus({ preventScroll: true }); };
const api = { $, trigger, list, later, stop, say, render, place, openModal, closeModal, getTasks: () => tasks, setTasks: (next) => { tasks = next; render(); } };
render();
const resetDemo = runDemo(api);
$('reset').addEventListener('click', () => { timers.forEach(clearTimeout); timers.clear(); resetDemo(); tasks = base.map((task) => ({ ...task })); render(); trigger.disabled = false; say(initialHint); $('reset').focus({ preventScroll: true }); });
document.body.dataset.theme = new URLSearchParams(location.search).get('theme') === 'dark' ? 'dark' : 'light';
document.body.dataset.ready = 'true';
document.body.dataset.demo = kind;
})("popconfirm","删除前确认一次;删除后仍可撤销。",function popconfirmDemo({ $, trigger, say, place, getTasks, setTasks }) {
const panel = $('popconfirm'); const toast = $('toast'); let removed = null;
trigger.setAttribute('aria-haspopup', 'dialog'); trigger.setAttribute('aria-expanded', 'false');
const hide = (focus = true) => { panel.hidden = true; trigger.setAttribute('aria-expanded', 'false'); if (focus) trigger.focus(); };
trigger.addEventListener('click', () => { if (!panel.hidden) { hide(); return; } panel.hidden = false; trigger.setAttribute('aria-expanded', 'true'); place(panel); $('cancel').focus(); say('尚未删除,请确认或保留任务。'); });
$('cancel').addEventListener('click', () => { hide(); say('已取消删除,全部任务均保留。'); });
$('confirm').addEventListener('click', () => { removed = getTasks().find((task) => task.id === 1); setTasks(getTasks().filter((task) => task.id !== 1)); hide(); trigger.disabled = true; toast.hidden = false; $('undo').focus(); say('第一项任务已删除,可以撤销。'); });
$('undo').addEventListener('click', () => { if (removed) setTasks([removed, ...getTasks()]); removed = null; toast.hidden = true; trigger.disabled = false; trigger.focus(); say('已撤销删除,任务恢复。'); });
document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !panel.hidden) { hide(); say('已取消删除,全部任务均保留。'); } });
document.addEventListener('pointerdown', (event) => { if (!panel.hidden && !panel.contains(event.target) && !trigger.contains(event.target)) { hide(false); say('已取消删除,全部任务均保留。'); } });
addEventListener('resize', () => { if (!panel.hidden) place(panel); });
return () => { hide(false); toast.hidden = true; removed = null; };
});</script></body></html>
5. Tooltip:解释一个词或图标
适合解释“优先级”等小概念,内容应短。Tooltip 本身不接收焦点,关键按钮和必须阅读的说明不宜藏在其中;这是 W3C Tooltip 模式的重要边界,该文档目前也注明模式仍在讨论中。
给 AI 的提示词:
在优先级说明图标上加入 Tooltip,鼠标悬停或键盘聚焦时显示,Esc 可收起。让触发元素关联说明文本;气泡内不放按钮和输入框。手机提供可点按的说明入口,关键含义同时保留在可见标签里。
**亲手验收:**用鼠标和 Tab 分别打开;检查提示没有抢走焦点。手机点按后能关闭,长文字也不会被屏幕边缘裁掉。
Tooltip · 气泡提示
悬停或聚焦图标,用 Escape 关闭提示。
可直接操作。数据只在此演示区域临时变化,点击“重新开始”恢复初始状态。

查看完整可运行源码
与上方演示使用相同源码,无额外依赖;保存为 .html 文件即可打开。
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><meta name="color-scheme" content="light dark"><title>Tooltip · 一点提示,刚刚好 — 交互演示</title><style>
:root{color-scheme:light;--bg:#f5f5f2;--paper:#fff;--ink:#242424;--muted:#85857f;--line:#e9e9e3;--soft:#f8f8f5;--accent:#df642b;--tint:#fff2e9;--shadow:0 12px 45px #2221;--overlay:#16161655;font-family:Arial,"PingFang SC","Microsoft YaHei",sans-serif}*{box-sizing:border-box}html,body{margin:0;min-width:0;min-height:100%;width:100%}body{background:var(--bg);color:var(--ink);font-size:12px;line-height:1.5}body[data-theme=dark]{color-scheme:dark;--bg:#171818;--paper:#222323;--ink:#ededeb;--muted:#aaa9a2;--line:#3b3b37;--soft:#292a28;--accent:#f39158;--tint:#413025;--shadow:0 12px 45px #0005;--overlay:#0009}button,input,textarea{font:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.45}button,input,textarea{outline:none}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button{border:0}button:active{transform:translateY(1px)}[hidden]{display:none!important}.stage{width:min(100%,680px);height:480px;margin:0 auto;padding:27px 30px 18px;position:relative;overflow:hidden;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:7px;font-size:10px;letter-spacing:1.5px;font-weight:700;color:var(--muted);margin-bottom:21px}.brand-mark{height:8px;width:8px;background:var(--accent)}.workspace{background:var(--paper);border:1px solid var(--line);border-radius:11px;box-shadow:0 3px 18px #22200008;padding:20px 20px 0;flex:1;min-height:0;display:flex;flex-direction:column}.workspace-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:19px}h1{font-size:21px;letter-spacing:-.8px;margin:0;font-weight:650}h2{font-size:18px;line-height:1.4;margin:10px 0 8px;font-weight:650;letter-spacing:-.4px}h3{font-size:12px;margin:20px 0 8px}p{margin:0}.subtitle{font-size:10px;color:var(--muted);margin-top:4px}.primary,.secondary{border-radius:5px;padding:8px 11px;white-space:nowrap;font-weight:600;font-size:11px;line-height:1.3}.primary{color:var(--paper);background:var(--ink)}.primary:hover{opacity:.85}.secondary{color:var(--ink);background:var(--paper);border:1px solid var(--line)}.secondary:hover{background:var(--soft)}.primary.accent-button{color:var(--accent);background:var(--tint);border:1px solid color-mix(in srgb,var(--accent) 15%,transparent)}.toolbar{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding:0 0 11px}.tab{font-size:10px;font-weight:600}.count{margin-left:6px;padding:1px 5px;border-radius:4px;background:var(--soft);color:var(--muted);font-size:9px}.toolbar-note{font-size:9px;color:var(--muted)}.task-list{padding:0;margin:0;list-style:none;overflow:auto;min-height:0;flex:1}.task{min-height:55px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:11px}.task:last-child{border-bottom:0}.check{width:14px;height:14px;flex:0 0 14px;border:1px solid #d6d6ce;border-radius:4px;font-size:10px;display:grid;place-items:center}.task.done .check{border-color:var(--accent);color:var(--accent);background:var(--tint)}.task.done .task-title{text-decoration:line-through;color:var(--muted)}.task-main{min-width:0;flex:1}.task-title{font-size:11px;font-weight:550;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.task-meta{font-size:9px;color:var(--muted);margin-top:3px}.priority{font-size:9px;padding:3px 5px;border-radius:4px;color:var(--accent);background:var(--tint);white-space:nowrap}.avatar{width:23px;height:23px;display:grid;place-items:center;flex-shrink:0;border-radius:50%;background:var(--soft);border:1px solid var(--line);color:var(--muted);font-size:9px}.workspace-foot{border-top:1px solid var(--line);color:var(--muted);padding:11px 0;font-size:9px;display:flex;align-items:center;gap:6px}.live-dot{width:4px;height:4px;border-radius:50%;background:var(--accent)}.demo-foot{display:flex;align-items:flex-start;gap:15px;justify-content:space-between;padding:15px 1px 0;min-height:53px}.demo-label{font-size:9px;color:var(--muted);letter-spacing:.6px;font-weight:600}.status{font-size:10px;color:var(--muted);margin-top:4px;min-height:15px}.text-button{color:var(--accent);background:none;padding:3px 4px;font-weight:600;font-size:11px}.reset{font-size:10px;color:var(--muted);border-bottom:1px solid var(--line);background:none;padding:3px 0;white-space:nowrap}.floating{position:fixed;z-index:10;background:var(--paper);border:1px solid var(--line);border-radius:9px;box-shadow:var(--shadow)}.toast{bottom:75px;right:max(22px,calc((100vw - 580px)/2));display:flex;align-items:center;gap:11px;padding:12px 15px;max-width:calc(100vw - 32px);animation:appear .18s ease-out;font-size:11px}.success-mark{color:var(--accent);font-size:15px}.hover-card{width:275px;padding:18px}.profile{display:flex;align-items:center;gap:10px;margin-bottom:13px}.profile strong{font-size:13px}.profile p{font-size:9px;color:var(--muted);margin-top:3px}.large{width:35px;height:35px;background:var(--tint);color:var(--accent);border:0;font-size:13px}.online{margin-left:auto;width:6px;height:6px;border-radius:50%;background:var(--accent)}.hover-card>p{color:var(--muted);font-size:10px}.profile-stats{display:flex;justify-content:space-between;border-top:1px solid var(--line);padding-top:12px;margin-top:15px;font-size:9px;color:var(--muted)}.profile-stats strong{display:block;color:var(--ink);font-size:16px;font-weight:500;margin-bottom:3px}.popconfirm{width:270px;padding:18px}.eyebrow{font-size:9px;font-weight:600;color:var(--accent);letter-spacing:1px}.popconfirm p,.center-dialog>p,.tour>p{font-size:11px;color:var(--muted);line-height:1.8}.button-row{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.panel-top{display:flex;justify-content:space-between;align-items:center;gap:10px}.icon-button{width:27px;height:27px;border-radius:5px;background:var(--soft);color:var(--muted);font-size:20px;line-height:1}.icon-button:hover{color:var(--ink)}dialog{color:var(--ink);background:var(--paper);border:1px solid var(--line);box-shadow:var(--shadow);padding:24px;max-width:calc(100vw - 32px);max-height:calc(100dvh - 32px)}dialog::backdrop{background:var(--overlay);backdrop-filter:blur(2px)}.drawer{position:fixed;inset:16px 16px 16px auto;margin:0;width:320px;height:calc(100dvh - 32px);border-radius:10px;animation:slide-in .2s ease-out;overflow:auto}.muted{color:var(--muted);font-size:11px}.detail-grid{display:grid;grid-template-columns:80px 1fr;gap:12px 8px;margin-top:25px;padding-bottom:20px;border-bottom:1px solid var(--line);font-size:10px}.detail-grid>span{color:var(--muted)}.detail-grid strong{font-weight:500}.body-copy{font-size:11px;line-height:1.9;color:var(--muted)}.subtasks{background:var(--soft);border-radius:5px;padding:10px;margin-top:14px;font-size:10px;color:var(--muted);line-height:2}.panel-actions{display:flex;justify-content:flex-end;margin-top:20px}.center-dialog{width:355px;border-radius:11px;animation:appear .18s ease-out}.form-dialog{width:360px;padding:20px}.form-dialog h2{margin:8px 0 16px}.form-dialog label{display:block;font-size:10px;font-weight:600;margin:12px 0 6px}.form-dialog .failure-option{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:9px;font-weight:400}.failure-option input{width:12px;height:12px;padding:0;accent-color:var(--accent)}.optional{font-weight:400;color:var(--muted);margin-left:5px}input,textarea{background:var(--paper);color:var(--ink);border:1px solid var(--line);border-radius:5px;width:100%;padding:9px 10px;font-size:11px;resize:vertical}input::placeholder,textarea::placeholder{color:var(--muted)}.accent{color:var(--accent)}.form-error{font-size:10px;color:var(--accent);margin-top:7px}.tooltip{z-index:20;position:fixed;width:max-content;max-width:calc(100vw - 32px);padding:8px 11px;border-radius:5px;background:var(--ink);color:var(--paper);font-size:10px;pointer-events:none;box-shadow:var(--shadow)}.tooltip-arrow{position:absolute;top:-4px;left:50%;width:8px;height:8px;transform:rotate(45deg);background:var(--ink)}.empty{text-align:center;color:var(--muted);padding:40px 10px;list-style:none;font-size:11px}.tour-blocker{position:fixed;inset:0;z-index:30}.tour-spotlight{position:fixed;z-index:31;border:2px solid var(--accent);border-radius:7px;box-shadow:0 0 0 9999px var(--overlay);pointer-events:none;transition:all .18s}.tour{z-index:32;width:275px;padding:16px}.tour-actions{display:flex;align-items:center;justify-content:space-between;margin-top:16px;gap:8px}.tour-actions .button-row{margin:0;gap:5px}.tour-actions .primary,.tour-actions .secondary{padding:7px 9px;font-size:10px}@keyframes appear{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@keyframes slide-in{from{opacity:.5;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@media(max-width:440px){.stage{padding:21px 16px 15px}.workspace{padding:17px 14px 0}.brand{margin-bottom:18px}.workspace-head{gap:7px}h1{font-size:19px}.workspace-head>.primary{font-size:10px;padding:8px}.toolbar-note{display:none}.task{gap:8px}.task-title{font-size:10px}.avatar{width:21px;height:21px}.priority{font-size:8px}.demo-foot{gap:8px}.status{font-size:9px}.drawer{width:calc(100vw - 32px)}.toast{right:16px}.tour{width:260px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
</style></head>
<body><main class="stage"><div class="brand"><span class="brand-mark"></span> SMALL STEPS / 任务工作台</div><section class="workspace" aria-label="每日任务工作台"><header class="workspace-head"><div data-tour="heading"><h1>今天,做好这几件事。</h1><p class="subtitle">把注意力留给真正重要的事情</p></div><button class="primary accent-button" data-testid="trigger">什么是优先级?</button></header><div class="toolbar" data-tour="toolbar"><span class="tab">全部任务<span class="count" data-testid="task-count">3</span></span><span class="toolbar-note">按优先级排列 ↕</span></div><ul class="task-list" data-testid="task-list" aria-label="任务列表"></ul><div class="workspace-foot"><span class="live-dot"></span> 每一步,都算数。<span style="margin-left:auto">今日计划 / 03</span></div></section><footer class="demo-foot"><div><div class="demo-label">05 / TOOLTIP <span style="margin-left:7px;font-weight:400">一点提示,刚刚好</span></div><p class="status" data-testid="status" role="status" aria-live="polite">悬停或用 Tab 聚焦按钮,按 Esc 收起。</p></div><button class="reset" data-testid="reset">↺ 重置</button></footer></main><div class="tooltip" id="priority-tip" data-testid="tooltip" role="tooltip" hidden>高优先级任务建议今天先完成。<span class="tooltip-arrow"></span></div><script>(function sharedRuntime(kind, initialHint, runDemo) {
'use strict';
const $ = (id) => document.querySelector('[data-testid="' + id + '"]');
const trigger = $('trigger');
const status = $('status');
const list = $('task-list');
const base = [{ id: 1, title: '确认首页视觉稿', meta: '设计迭代 · 今天', owner: '林', priority: true }, { id: 2, title: '整理用户访谈记录', meta: '用户研究 · 明天', owner: '周' }, { id: 3, title: '准备周五的产品分享', meta: '团队协作 · 周五', owner: '陈' }];
let tasks = base.map((task) => ({ ...task }));
const timers = new Set();
const later = (callback, delay) => { const timer = setTimeout(() => { timers.delete(timer); callback(); }, delay); timers.add(timer); return timer; };
const stop = (timer) => { clearTimeout(timer); timers.delete(timer); };
const say = (text) => { status.textContent = text; };
const render = () => {
list.replaceChildren();
tasks.forEach((task) => {
const row = document.createElement('li'); row.className = 'task' + (task.done ? ' done' : ''); row.dataset.testid = 'task-row-' + task.id; row.dataset.taskId = String(task.id);
const check = document.createElement('span'); check.className = 'check'; check.textContent = task.done ? '✓' : ''; check.setAttribute('aria-hidden', 'true');
const main = document.createElement('div'); main.className = 'task-main';
const title = document.createElement('div'); title.className = 'task-title'; title.textContent = task.title;
const meta = document.createElement('div'); meta.className = 'task-meta'; meta.textContent = task.meta;
main.append(title, meta); row.append(check, main);
if (task.priority) { const badge = document.createElement('span'); badge.className = 'priority'; badge.textContent = '高优先级'; row.append(badge); }
const owner = document.createElement('span'); owner.className = 'avatar'; owner.textContent = task.owner; owner.setAttribute('aria-label', '负责人 ' + task.owner); row.append(owner); list.append(row);
});
if (!tasks.length) { const empty = document.createElement('li'); empty.className = 'empty'; empty.dataset.testid = 'empty'; empty.textContent = '任务都处理好了,留一点时间给自己。'; list.append(empty); }
$('task-count').textContent = String(tasks.length);
};
const place = (element, anchor = trigger, gap = 10) => {
const a = anchor.getBoundingClientRect(); const r = element.getBoundingClientRect();
element.style.left = Math.max(12, Math.min(a.right - r.width, innerWidth - r.width - 12)) + 'px';
element.style.top = Math.max(12, Math.min(a.bottom + gap, innerHeight - r.height - 12)) + 'px';
};
const openModal = (modal) => { if (!modal.open) modal.showModal(); trigger.setAttribute('aria-expanded', 'true'); };
const closeModal = (modal) => { if (modal.open) modal.close(); trigger.setAttribute('aria-expanded', 'false'); trigger.focus({ preventScroll: true }); };
const api = { $, trigger, list, later, stop, say, render, place, openModal, closeModal, getTasks: () => tasks, setTasks: (next) => { tasks = next; render(); } };
render();
const resetDemo = runDemo(api);
$('reset').addEventListener('click', () => { timers.forEach(clearTimeout); timers.clear(); resetDemo(); tasks = base.map((task) => ({ ...task })); render(); trigger.disabled = false; say(initialHint); $('reset').focus({ preventScroll: true }); });
document.body.dataset.theme = new URLSearchParams(location.search).get('theme') === 'dark' ? 'dark' : 'light';
document.body.dataset.ready = 'true';
document.body.dataset.demo = kind;
})("tooltip","悬停或用 Tab 聚焦按钮,按 Esc 收起。",function tooltipDemo({ $, trigger, place, say }) {
const tooltip = $('tooltip');
const show = () => { tooltip.hidden = false; trigger.setAttribute('aria-describedby', 'priority-tip'); place(tooltip); say('提示已显示。按 Esc 收起。'); };
const hide = () => { tooltip.hidden = true; trigger.removeAttribute('aria-describedby'); };
trigger.addEventListener('pointerenter', show); trigger.addEventListener('pointerleave', hide); trigger.addEventListener('focus', show); trigger.addEventListener('blur', hide);
trigger.addEventListener('click', show);
document.addEventListener('pointerdown', (event) => { if (!tooltip.hidden && !trigger.contains(event.target)) { hide(); say('提示已收起。'); } });
document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { hide(); say('提示已收起。'); } });
addEventListener('resize', () => { if (!tooltip.hidden) place(tooltip); });
return hide;
});</script></body></html>
6. Alert Dialog:高风险操作,明确后果再确认
清空任务比删除一条更危险,需要更醒目的警告。阻止误操作的重点是明确范围、后果与取消路径,而非把用户困在弹框里。
给 AI 的提示词:
清空任务前打开 Alert Dialog,明确提示操作范围和不可恢复后果。提供“取消”和含具体动作的确认按钮,初始焦点放在取消上;Esc 可取消,点遮罩不得执行清空。仅在明确确认后修改数据。
**亲手验收:**先用 Esc、取消按钮退出,确认任务仍在,再试确认路径。W3C 模态对话框指南建议难以撤销的流程优先聚焦较安全的动作,并保留可见关闭入口。
Alert Dialog · 警告确认
尝试清空列表;只有明确确认才会修改演示数据。
可直接操作。数据只在此演示区域临时变化,点击“重新开始”恢复初始状态。

查看完整可运行源码
与上方演示使用相同源码,无额外依赖;保存为 .html 文件即可打开。
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><meta name="color-scheme" content="light dark"><title>Alert Dialog · 重要操作,认真确认 — 交互演示</title><style>
:root{color-scheme:light;--bg:#f5f5f2;--paper:#fff;--ink:#242424;--muted:#85857f;--line:#e9e9e3;--soft:#f8f8f5;--accent:#df642b;--tint:#fff2e9;--shadow:0 12px 45px #2221;--overlay:#16161655;font-family:Arial,"PingFang SC","Microsoft YaHei",sans-serif}*{box-sizing:border-box}html,body{margin:0;min-width:0;min-height:100%;width:100%}body{background:var(--bg);color:var(--ink);font-size:12px;line-height:1.5}body[data-theme=dark]{color-scheme:dark;--bg:#171818;--paper:#222323;--ink:#ededeb;--muted:#aaa9a2;--line:#3b3b37;--soft:#292a28;--accent:#f39158;--tint:#413025;--shadow:0 12px 45px #0005;--overlay:#0009}button,input,textarea{font:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.45}button,input,textarea{outline:none}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button{border:0}button:active{transform:translateY(1px)}[hidden]{display:none!important}.stage{width:min(100%,680px);height:480px;margin:0 auto;padding:27px 30px 18px;position:relative;overflow:hidden;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:7px;font-size:10px;letter-spacing:1.5px;font-weight:700;color:var(--muted);margin-bottom:21px}.brand-mark{height:8px;width:8px;background:var(--accent)}.workspace{background:var(--paper);border:1px solid var(--line);border-radius:11px;box-shadow:0 3px 18px #22200008;padding:20px 20px 0;flex:1;min-height:0;display:flex;flex-direction:column}.workspace-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:19px}h1{font-size:21px;letter-spacing:-.8px;margin:0;font-weight:650}h2{font-size:18px;line-height:1.4;margin:10px 0 8px;font-weight:650;letter-spacing:-.4px}h3{font-size:12px;margin:20px 0 8px}p{margin:0}.subtitle{font-size:10px;color:var(--muted);margin-top:4px}.primary,.secondary{border-radius:5px;padding:8px 11px;white-space:nowrap;font-weight:600;font-size:11px;line-height:1.3}.primary{color:var(--paper);background:var(--ink)}.primary:hover{opacity:.85}.secondary{color:var(--ink);background:var(--paper);border:1px solid var(--line)}.secondary:hover{background:var(--soft)}.primary.accent-button{color:var(--accent);background:var(--tint);border:1px solid color-mix(in srgb,var(--accent) 15%,transparent)}.toolbar{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding:0 0 11px}.tab{font-size:10px;font-weight:600}.count{margin-left:6px;padding:1px 5px;border-radius:4px;background:var(--soft);color:var(--muted);font-size:9px}.toolbar-note{font-size:9px;color:var(--muted)}.task-list{padding:0;margin:0;list-style:none;overflow:auto;min-height:0;flex:1}.task{min-height:55px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:11px}.task:last-child{border-bottom:0}.check{width:14px;height:14px;flex:0 0 14px;border:1px solid #d6d6ce;border-radius:4px;font-size:10px;display:grid;place-items:center}.task.done .check{border-color:var(--accent);color:var(--accent);background:var(--tint)}.task.done .task-title{text-decoration:line-through;color:var(--muted)}.task-main{min-width:0;flex:1}.task-title{font-size:11px;font-weight:550;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.task-meta{font-size:9px;color:var(--muted);margin-top:3px}.priority{font-size:9px;padding:3px 5px;border-radius:4px;color:var(--accent);background:var(--tint);white-space:nowrap}.avatar{width:23px;height:23px;display:grid;place-items:center;flex-shrink:0;border-radius:50%;background:var(--soft);border:1px solid var(--line);color:var(--muted);font-size:9px}.workspace-foot{border-top:1px solid var(--line);color:var(--muted);padding:11px 0;font-size:9px;display:flex;align-items:center;gap:6px}.live-dot{width:4px;height:4px;border-radius:50%;background:var(--accent)}.demo-foot{display:flex;align-items:flex-start;gap:15px;justify-content:space-between;padding:15px 1px 0;min-height:53px}.demo-label{font-size:9px;color:var(--muted);letter-spacing:.6px;font-weight:600}.status{font-size:10px;color:var(--muted);margin-top:4px;min-height:15px}.text-button{color:var(--accent);background:none;padding:3px 4px;font-weight:600;font-size:11px}.reset{font-size:10px;color:var(--muted);border-bottom:1px solid var(--line);background:none;padding:3px 0;white-space:nowrap}.floating{position:fixed;z-index:10;background:var(--paper);border:1px solid var(--line);border-radius:9px;box-shadow:var(--shadow)}.toast{bottom:75px;right:max(22px,calc((100vw - 580px)/2));display:flex;align-items:center;gap:11px;padding:12px 15px;max-width:calc(100vw - 32px);animation:appear .18s ease-out;font-size:11px}.success-mark{color:var(--accent);font-size:15px}.hover-card{width:275px;padding:18px}.profile{display:flex;align-items:center;gap:10px;margin-bottom:13px}.profile strong{font-size:13px}.profile p{font-size:9px;color:var(--muted);margin-top:3px}.large{width:35px;height:35px;background:var(--tint);color:var(--accent);border:0;font-size:13px}.online{margin-left:auto;width:6px;height:6px;border-radius:50%;background:var(--accent)}.hover-card>p{color:var(--muted);font-size:10px}.profile-stats{display:flex;justify-content:space-between;border-top:1px solid var(--line);padding-top:12px;margin-top:15px;font-size:9px;color:var(--muted)}.profile-stats strong{display:block;color:var(--ink);font-size:16px;font-weight:500;margin-bottom:3px}.popconfirm{width:270px;padding:18px}.eyebrow{font-size:9px;font-weight:600;color:var(--accent);letter-spacing:1px}.popconfirm p,.center-dialog>p,.tour>p{font-size:11px;color:var(--muted);line-height:1.8}.button-row{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.panel-top{display:flex;justify-content:space-between;align-items:center;gap:10px}.icon-button{width:27px;height:27px;border-radius:5px;background:var(--soft);color:var(--muted);font-size:20px;line-height:1}.icon-button:hover{color:var(--ink)}dialog{color:var(--ink);background:var(--paper);border:1px solid var(--line);box-shadow:var(--shadow);padding:24px;max-width:calc(100vw - 32px);max-height:calc(100dvh - 32px)}dialog::backdrop{background:var(--overlay);backdrop-filter:blur(2px)}.drawer{position:fixed;inset:16px 16px 16px auto;margin:0;width:320px;height:calc(100dvh - 32px);border-radius:10px;animation:slide-in .2s ease-out;overflow:auto}.muted{color:var(--muted);font-size:11px}.detail-grid{display:grid;grid-template-columns:80px 1fr;gap:12px 8px;margin-top:25px;padding-bottom:20px;border-bottom:1px solid var(--line);font-size:10px}.detail-grid>span{color:var(--muted)}.detail-grid strong{font-weight:500}.body-copy{font-size:11px;line-height:1.9;color:var(--muted)}.subtasks{background:var(--soft);border-radius:5px;padding:10px;margin-top:14px;font-size:10px;color:var(--muted);line-height:2}.panel-actions{display:flex;justify-content:flex-end;margin-top:20px}.center-dialog{width:355px;border-radius:11px;animation:appear .18s ease-out}.form-dialog{width:360px;padding:20px}.form-dialog h2{margin:8px 0 16px}.form-dialog label{display:block;font-size:10px;font-weight:600;margin:12px 0 6px}.form-dialog .failure-option{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:9px;font-weight:400}.failure-option input{width:12px;height:12px;padding:0;accent-color:var(--accent)}.optional{font-weight:400;color:var(--muted);margin-left:5px}input,textarea{background:var(--paper);color:var(--ink);border:1px solid var(--line);border-radius:5px;width:100%;padding:9px 10px;font-size:11px;resize:vertical}input::placeholder,textarea::placeholder{color:var(--muted)}.accent{color:var(--accent)}.form-error{font-size:10px;color:var(--accent);margin-top:7px}.tooltip{z-index:20;position:fixed;width:max-content;max-width:calc(100vw - 32px);padding:8px 11px;border-radius:5px;background:var(--ink);color:var(--paper);font-size:10px;pointer-events:none;box-shadow:var(--shadow)}.tooltip-arrow{position:absolute;top:-4px;left:50%;width:8px;height:8px;transform:rotate(45deg);background:var(--ink)}.empty{text-align:center;color:var(--muted);padding:40px 10px;list-style:none;font-size:11px}.tour-blocker{position:fixed;inset:0;z-index:30}.tour-spotlight{position:fixed;z-index:31;border:2px solid var(--accent);border-radius:7px;box-shadow:0 0 0 9999px var(--overlay);pointer-events:none;transition:all .18s}.tour{z-index:32;width:275px;padding:16px}.tour-actions{display:flex;align-items:center;justify-content:space-between;margin-top:16px;gap:8px}.tour-actions .button-row{margin:0;gap:5px}.tour-actions .primary,.tour-actions .secondary{padding:7px 9px;font-size:10px}@keyframes appear{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@keyframes slide-in{from{opacity:.5;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@media(max-width:440px){.stage{padding:21px 16px 15px}.workspace{padding:17px 14px 0}.brand{margin-bottom:18px}.workspace-head{gap:7px}h1{font-size:19px}.workspace-head>.primary{font-size:10px;padding:8px}.toolbar-note{display:none}.task{gap:8px}.task-title{font-size:10px}.avatar{width:21px;height:21px}.priority{font-size:8px}.demo-foot{gap:8px}.status{font-size:9px}.drawer{width:calc(100vw - 32px)}.toast{right:16px}.tour{width:260px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
</style></head>
<body><main class="stage"><div class="brand"><span class="brand-mark"></span> SMALL STEPS / 任务工作台</div><section class="workspace" aria-label="每日任务工作台"><header class="workspace-head"><div data-tour="heading"><h1>今天,做好这几件事。</h1><p class="subtitle">把注意力留给真正重要的事情</p></div><button class="primary " data-testid="trigger">清空任务</button></header><div class="toolbar" data-tour="toolbar"><span class="tab">全部任务<span class="count" data-testid="task-count">3</span></span><span class="toolbar-note">按优先级排列 ↕</span></div><ul class="task-list" data-testid="task-list" aria-label="任务列表"></ul><div class="workspace-foot"><span class="live-dot"></span> 每一步,都算数。<span style="margin-left:auto">今日计划 / 03</span></div></section><footer class="demo-foot"><div><div class="demo-label">06 / ALERT DIALOG <span style="margin-left:7px;font-weight:400">重要操作,认真确认</span></div><p class="status" data-testid="status" role="status" aria-live="polite">这个操作需要明确确认,默认焦点在取消。</p></div><button class="reset" data-testid="reset">↺ 重置</button></footer></main><dialog class="center-dialog" data-testid="alert-dialog" role="alertdialog" aria-labelledby="alert-title" aria-describedby="alert-description"><span class="eyebrow">不可忽略的决定</span><h2 id="alert-title">清空今天的全部任务?</h2><p id="alert-description">列表中的 3 项任务将被移除。请确认它们已处理完毕。</p><div class="button-row"><button class="secondary" data-testid="cancel" autofocus>暂不清空</button><button class="primary" data-testid="confirm">确认清空</button></div></dialog><script>(function sharedRuntime(kind, initialHint, runDemo) {
'use strict';
const $ = (id) => document.querySelector('[data-testid="' + id + '"]');
const trigger = $('trigger');
const status = $('status');
const list = $('task-list');
const base = [{ id: 1, title: '确认首页视觉稿', meta: '设计迭代 · 今天', owner: '林', priority: true }, { id: 2, title: '整理用户访谈记录', meta: '用户研究 · 明天', owner: '周' }, { id: 3, title: '准备周五的产品分享', meta: '团队协作 · 周五', owner: '陈' }];
let tasks = base.map((task) => ({ ...task }));
const timers = new Set();
const later = (callback, delay) => { const timer = setTimeout(() => { timers.delete(timer); callback(); }, delay); timers.add(timer); return timer; };
const stop = (timer) => { clearTimeout(timer); timers.delete(timer); };
const say = (text) => { status.textContent = text; };
const render = () => {
list.replaceChildren();
tasks.forEach((task) => {
const row = document.createElement('li'); row.className = 'task' + (task.done ? ' done' : ''); row.dataset.testid = 'task-row-' + task.id; row.dataset.taskId = String(task.id);
const check = document.createElement('span'); check.className = 'check'; check.textContent = task.done ? '✓' : ''; check.setAttribute('aria-hidden', 'true');
const main = document.createElement('div'); main.className = 'task-main';
const title = document.createElement('div'); title.className = 'task-title'; title.textContent = task.title;
const meta = document.createElement('div'); meta.className = 'task-meta'; meta.textContent = task.meta;
main.append(title, meta); row.append(check, main);
if (task.priority) { const badge = document.createElement('span'); badge.className = 'priority'; badge.textContent = '高优先级'; row.append(badge); }
const owner = document.createElement('span'); owner.className = 'avatar'; owner.textContent = task.owner; owner.setAttribute('aria-label', '负责人 ' + task.owner); row.append(owner); list.append(row);
});
if (!tasks.length) { const empty = document.createElement('li'); empty.className = 'empty'; empty.dataset.testid = 'empty'; empty.textContent = '任务都处理好了,留一点时间给自己。'; list.append(empty); }
$('task-count').textContent = String(tasks.length);
};
const place = (element, anchor = trigger, gap = 10) => {
const a = anchor.getBoundingClientRect(); const r = element.getBoundingClientRect();
element.style.left = Math.max(12, Math.min(a.right - r.width, innerWidth - r.width - 12)) + 'px';
element.style.top = Math.max(12, Math.min(a.bottom + gap, innerHeight - r.height - 12)) + 'px';
};
const openModal = (modal) => { if (!modal.open) modal.showModal(); trigger.setAttribute('aria-expanded', 'true'); };
const closeModal = (modal) => { if (modal.open) modal.close(); trigger.setAttribute('aria-expanded', 'false'); trigger.focus({ preventScroll: true }); };
const api = { $, trigger, list, later, stop, say, render, place, openModal, closeModal, getTasks: () => tasks, setTasks: (next) => { tasks = next; render(); } };
render();
const resetDemo = runDemo(api);
$('reset').addEventListener('click', () => { timers.forEach(clearTimeout); timers.clear(); resetDemo(); tasks = base.map((task) => ({ ...task })); render(); trigger.disabled = false; say(initialHint); $('reset').focus({ preventScroll: true }); });
document.body.dataset.theme = new URLSearchParams(location.search).get('theme') === 'dark' ? 'dark' : 'light';
document.body.dataset.ready = 'true';
document.body.dataset.demo = kind;
})("alert-dialog","这个操作需要明确确认,默认焦点在取消。",function alertDemo({ $, trigger, say, openModal, closeModal, setTasks }) {
const modal = $('alert-dialog'); trigger.setAttribute('aria-haspopup', 'dialog');
trigger.addEventListener('click', () => { openModal(modal); $('cancel').focus(); say('任务尚未清空,请做出选择。'); });
const cancel = () => { closeModal(modal); say('已取消清空,3 项任务均保留。'); };
$('cancel').addEventListener('click', cancel);
modal.addEventListener('cancel', (event) => { event.preventDefault(); cancel(); });
$('confirm').addEventListener('click', () => { setTasks([]); closeModal(modal); trigger.disabled = true; $('reset').focus(); say('已确认清空全部任务,点击重置可重新体验。'); });
return () => closeModal(modal);
});</script></body></html>
7. Dialog:集中完成一个短表单
新建任务需要输入标题、检查错误、提交结果,更适合 Dialog。它承载的是一段完整操作,不能只生成一个好看的空壳。
给 AI 的提示词:
用 Dialog 完成新建任务。输入框有可见标签,空标题显示具体错误;校验或提交失败时保留输入,允许修正重试。处理中阻止重复提交,成功后再关闭并更新列表。支持取消、Esc、焦点约束和关闭后的焦点恢复。
**亲手验收:**先提交空值,再修正;确认失败不会清空内容。手机弹出软键盘后仍应够到错误提示和提交按钮。本例新增只在演示内存中,不等于接入了服务器存储。
Dialog · 模态表单
新建任务,试试空标题和失败状态,再正常提交。
可直接操作。数据只在此演示区域临时变化,点击“重新开始”恢复初始状态。

查看完整可运行源码
与上方演示使用相同源码,无额外依赖;保存为 .html 文件即可打开。
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><meta name="color-scheme" content="light dark"><title>Dialog · 专注完成一件事 — 交互演示</title><style>
:root{color-scheme:light;--bg:#f5f5f2;--paper:#fff;--ink:#242424;--muted:#85857f;--line:#e9e9e3;--soft:#f8f8f5;--accent:#df642b;--tint:#fff2e9;--shadow:0 12px 45px #2221;--overlay:#16161655;font-family:Arial,"PingFang SC","Microsoft YaHei",sans-serif}*{box-sizing:border-box}html,body{margin:0;min-width:0;min-height:100%;width:100%}body{background:var(--bg);color:var(--ink);font-size:12px;line-height:1.5}body[data-theme=dark]{color-scheme:dark;--bg:#171818;--paper:#222323;--ink:#ededeb;--muted:#aaa9a2;--line:#3b3b37;--soft:#292a28;--accent:#f39158;--tint:#413025;--shadow:0 12px 45px #0005;--overlay:#0009}button,input,textarea{font:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.45}button,input,textarea{outline:none}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button{border:0}button:active{transform:translateY(1px)}[hidden]{display:none!important}.stage{width:min(100%,680px);height:480px;margin:0 auto;padding:27px 30px 18px;position:relative;overflow:hidden;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:7px;font-size:10px;letter-spacing:1.5px;font-weight:700;color:var(--muted);margin-bottom:21px}.brand-mark{height:8px;width:8px;background:var(--accent)}.workspace{background:var(--paper);border:1px solid var(--line);border-radius:11px;box-shadow:0 3px 18px #22200008;padding:20px 20px 0;flex:1;min-height:0;display:flex;flex-direction:column}.workspace-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:19px}h1{font-size:21px;letter-spacing:-.8px;margin:0;font-weight:650}h2{font-size:18px;line-height:1.4;margin:10px 0 8px;font-weight:650;letter-spacing:-.4px}h3{font-size:12px;margin:20px 0 8px}p{margin:0}.subtitle{font-size:10px;color:var(--muted);margin-top:4px}.primary,.secondary{border-radius:5px;padding:8px 11px;white-space:nowrap;font-weight:600;font-size:11px;line-height:1.3}.primary{color:var(--paper);background:var(--ink)}.primary:hover{opacity:.85}.secondary{color:var(--ink);background:var(--paper);border:1px solid var(--line)}.secondary:hover{background:var(--soft)}.primary.accent-button{color:var(--accent);background:var(--tint);border:1px solid color-mix(in srgb,var(--accent) 15%,transparent)}.toolbar{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding:0 0 11px}.tab{font-size:10px;font-weight:600}.count{margin-left:6px;padding:1px 5px;border-radius:4px;background:var(--soft);color:var(--muted);font-size:9px}.toolbar-note{font-size:9px;color:var(--muted)}.task-list{padding:0;margin:0;list-style:none;overflow:auto;min-height:0;flex:1}.task{min-height:55px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:11px}.task:last-child{border-bottom:0}.check{width:14px;height:14px;flex:0 0 14px;border:1px solid #d6d6ce;border-radius:4px;font-size:10px;display:grid;place-items:center}.task.done .check{border-color:var(--accent);color:var(--accent);background:var(--tint)}.task.done .task-title{text-decoration:line-through;color:var(--muted)}.task-main{min-width:0;flex:1}.task-title{font-size:11px;font-weight:550;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.task-meta{font-size:9px;color:var(--muted);margin-top:3px}.priority{font-size:9px;padding:3px 5px;border-radius:4px;color:var(--accent);background:var(--tint);white-space:nowrap}.avatar{width:23px;height:23px;display:grid;place-items:center;flex-shrink:0;border-radius:50%;background:var(--soft);border:1px solid var(--line);color:var(--muted);font-size:9px}.workspace-foot{border-top:1px solid var(--line);color:var(--muted);padding:11px 0;font-size:9px;display:flex;align-items:center;gap:6px}.live-dot{width:4px;height:4px;border-radius:50%;background:var(--accent)}.demo-foot{display:flex;align-items:flex-start;gap:15px;justify-content:space-between;padding:15px 1px 0;min-height:53px}.demo-label{font-size:9px;color:var(--muted);letter-spacing:.6px;font-weight:600}.status{font-size:10px;color:var(--muted);margin-top:4px;min-height:15px}.text-button{color:var(--accent);background:none;padding:3px 4px;font-weight:600;font-size:11px}.reset{font-size:10px;color:var(--muted);border-bottom:1px solid var(--line);background:none;padding:3px 0;white-space:nowrap}.floating{position:fixed;z-index:10;background:var(--paper);border:1px solid var(--line);border-radius:9px;box-shadow:var(--shadow)}.toast{bottom:75px;right:max(22px,calc((100vw - 580px)/2));display:flex;align-items:center;gap:11px;padding:12px 15px;max-width:calc(100vw - 32px);animation:appear .18s ease-out;font-size:11px}.success-mark{color:var(--accent);font-size:15px}.hover-card{width:275px;padding:18px}.profile{display:flex;align-items:center;gap:10px;margin-bottom:13px}.profile strong{font-size:13px}.profile p{font-size:9px;color:var(--muted);margin-top:3px}.large{width:35px;height:35px;background:var(--tint);color:var(--accent);border:0;font-size:13px}.online{margin-left:auto;width:6px;height:6px;border-radius:50%;background:var(--accent)}.hover-card>p{color:var(--muted);font-size:10px}.profile-stats{display:flex;justify-content:space-between;border-top:1px solid var(--line);padding-top:12px;margin-top:15px;font-size:9px;color:var(--muted)}.profile-stats strong{display:block;color:var(--ink);font-size:16px;font-weight:500;margin-bottom:3px}.popconfirm{width:270px;padding:18px}.eyebrow{font-size:9px;font-weight:600;color:var(--accent);letter-spacing:1px}.popconfirm p,.center-dialog>p,.tour>p{font-size:11px;color:var(--muted);line-height:1.8}.button-row{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.panel-top{display:flex;justify-content:space-between;align-items:center;gap:10px}.icon-button{width:27px;height:27px;border-radius:5px;background:var(--soft);color:var(--muted);font-size:20px;line-height:1}.icon-button:hover{color:var(--ink)}dialog{color:var(--ink);background:var(--paper);border:1px solid var(--line);box-shadow:var(--shadow);padding:24px;max-width:calc(100vw - 32px);max-height:calc(100dvh - 32px)}dialog::backdrop{background:var(--overlay);backdrop-filter:blur(2px)}.drawer{position:fixed;inset:16px 16px 16px auto;margin:0;width:320px;height:calc(100dvh - 32px);border-radius:10px;animation:slide-in .2s ease-out;overflow:auto}.muted{color:var(--muted);font-size:11px}.detail-grid{display:grid;grid-template-columns:80px 1fr;gap:12px 8px;margin-top:25px;padding-bottom:20px;border-bottom:1px solid var(--line);font-size:10px}.detail-grid>span{color:var(--muted)}.detail-grid strong{font-weight:500}.body-copy{font-size:11px;line-height:1.9;color:var(--muted)}.subtasks{background:var(--soft);border-radius:5px;padding:10px;margin-top:14px;font-size:10px;color:var(--muted);line-height:2}.panel-actions{display:flex;justify-content:flex-end;margin-top:20px}.center-dialog{width:355px;border-radius:11px;animation:appear .18s ease-out}.form-dialog{width:360px;padding:20px}.form-dialog h2{margin:8px 0 16px}.form-dialog label{display:block;font-size:10px;font-weight:600;margin:12px 0 6px}.form-dialog .failure-option{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:9px;font-weight:400}.failure-option input{width:12px;height:12px;padding:0;accent-color:var(--accent)}.optional{font-weight:400;color:var(--muted);margin-left:5px}input,textarea{background:var(--paper);color:var(--ink);border:1px solid var(--line);border-radius:5px;width:100%;padding:9px 10px;font-size:11px;resize:vertical}input::placeholder,textarea::placeholder{color:var(--muted)}.accent{color:var(--accent)}.form-error{font-size:10px;color:var(--accent);margin-top:7px}.tooltip{z-index:20;position:fixed;width:max-content;max-width:calc(100vw - 32px);padding:8px 11px;border-radius:5px;background:var(--ink);color:var(--paper);font-size:10px;pointer-events:none;box-shadow:var(--shadow)}.tooltip-arrow{position:absolute;top:-4px;left:50%;width:8px;height:8px;transform:rotate(45deg);background:var(--ink)}.empty{text-align:center;color:var(--muted);padding:40px 10px;list-style:none;font-size:11px}.tour-blocker{position:fixed;inset:0;z-index:30}.tour-spotlight{position:fixed;z-index:31;border:2px solid var(--accent);border-radius:7px;box-shadow:0 0 0 9999px var(--overlay);pointer-events:none;transition:all .18s}.tour{z-index:32;width:275px;padding:16px}.tour-actions{display:flex;align-items:center;justify-content:space-between;margin-top:16px;gap:8px}.tour-actions .button-row{margin:0;gap:5px}.tour-actions .primary,.tour-actions .secondary{padding:7px 9px;font-size:10px}@keyframes appear{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@keyframes slide-in{from{opacity:.5;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@media(max-width:440px){.stage{padding:21px 16px 15px}.workspace{padding:17px 14px 0}.brand{margin-bottom:18px}.workspace-head{gap:7px}h1{font-size:19px}.workspace-head>.primary{font-size:10px;padding:8px}.toolbar-note{display:none}.task{gap:8px}.task-title{font-size:10px}.avatar{width:21px;height:21px}.priority{font-size:8px}.demo-foot{gap:8px}.status{font-size:9px}.drawer{width:calc(100vw - 32px)}.toast{right:16px}.tour{width:260px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
</style></head>
<body><main class="stage"><div class="brand"><span class="brand-mark"></span> SMALL STEPS / 任务工作台</div><section class="workspace" aria-label="每日任务工作台"><header class="workspace-head"><div data-tour="heading"><h1>今天,做好这几件事。</h1><p class="subtitle">把注意力留给真正重要的事情</p></div><button class="primary " data-testid="trigger">+ 新建任务</button></header><div class="toolbar" data-tour="toolbar"><span class="tab">全部任务<span class="count" data-testid="task-count">3</span></span><span class="toolbar-note">按优先级排列 ↕</span></div><ul class="task-list" data-testid="task-list" aria-label="任务列表"></ul><div class="workspace-foot"><span class="live-dot"></span> 每一步,都算数。<span style="margin-left:auto">今日计划 / 03</span></div></section><footer class="demo-foot"><div><div class="demo-label">07 / DIALOG <span style="margin-left:7px;font-weight:400">专注完成一件事</span></div><p class="status" data-testid="status" role="status" aria-live="polite">输入任务标题,提交后立即回到列表。</p></div><button class="reset" data-testid="reset">↺ 重置</button></footer></main><dialog class="center-dialog form-dialog" data-testid="dialog" aria-labelledby="dialog-title"><div class="panel-top"><span class="eyebrow">给想法一个位置</span><button class="icon-button" type="button" data-testid="close" aria-label="关闭新建任务">×</button></div><h2 id="dialog-title">新建任务</h2><form data-testid="task-form"><label for="task-title">任务标题 <span class="accent">*</span></label><input id="task-title" data-testid="title-input" name="title" required maxlength="48" autocomplete="off" placeholder="下一件想完成的事" autofocus><label for="task-description">补充说明 <span class="optional">选填</span></label><textarea id="task-description" data-testid="description-input" name="description" rows="2" maxlength="160" placeholder="记录一点背景信息"></textarea><p class="form-error" data-testid="form-error" role="alert" hidden>请填写任务标题。</p><div class="button-row"><button type="button" class="secondary" data-testid="cancel">取消</button><button class="primary" type="submit" data-testid="confirm">创建任务</button></div></form></dialog><script>(function sharedRuntime(kind, initialHint, runDemo) {
'use strict';
const $ = (id) => document.querySelector('[data-testid="' + id + '"]');
const trigger = $('trigger');
const status = $('status');
const list = $('task-list');
const base = [{ id: 1, title: '确认首页视觉稿', meta: '设计迭代 · 今天', owner: '林', priority: true }, { id: 2, title: '整理用户访谈记录', meta: '用户研究 · 明天', owner: '周' }, { id: 3, title: '准备周五的产品分享', meta: '团队协作 · 周五', owner: '陈' }];
let tasks = base.map((task) => ({ ...task }));
const timers = new Set();
const later = (callback, delay) => { const timer = setTimeout(() => { timers.delete(timer); callback(); }, delay); timers.add(timer); return timer; };
const stop = (timer) => { clearTimeout(timer); timers.delete(timer); };
const say = (text) => { status.textContent = text; };
const render = () => {
list.replaceChildren();
tasks.forEach((task) => {
const row = document.createElement('li'); row.className = 'task' + (task.done ? ' done' : ''); row.dataset.testid = 'task-row-' + task.id; row.dataset.taskId = String(task.id);
const check = document.createElement('span'); check.className = 'check'; check.textContent = task.done ? '✓' : ''; check.setAttribute('aria-hidden', 'true');
const main = document.createElement('div'); main.className = 'task-main';
const title = document.createElement('div'); title.className = 'task-title'; title.textContent = task.title;
const meta = document.createElement('div'); meta.className = 'task-meta'; meta.textContent = task.meta;
main.append(title, meta); row.append(check, main);
if (task.priority) { const badge = document.createElement('span'); badge.className = 'priority'; badge.textContent = '高优先级'; row.append(badge); }
const owner = document.createElement('span'); owner.className = 'avatar'; owner.textContent = task.owner; owner.setAttribute('aria-label', '负责人 ' + task.owner); row.append(owner); list.append(row);
});
if (!tasks.length) { const empty = document.createElement('li'); empty.className = 'empty'; empty.dataset.testid = 'empty'; empty.textContent = '任务都处理好了,留一点时间给自己。'; list.append(empty); }
$('task-count').textContent = String(tasks.length);
};
const place = (element, anchor = trigger, gap = 10) => {
const a = anchor.getBoundingClientRect(); const r = element.getBoundingClientRect();
element.style.left = Math.max(12, Math.min(a.right - r.width, innerWidth - r.width - 12)) + 'px';
element.style.top = Math.max(12, Math.min(a.bottom + gap, innerHeight - r.height - 12)) + 'px';
};
const openModal = (modal) => { if (!modal.open) modal.showModal(); trigger.setAttribute('aria-expanded', 'true'); };
const closeModal = (modal) => { if (modal.open) modal.close(); trigger.setAttribute('aria-expanded', 'false'); trigger.focus({ preventScroll: true }); };
const api = { $, trigger, list, later, stop, say, render, place, openModal, closeModal, getTasks: () => tasks, setTasks: (next) => { tasks = next; render(); } };
render();
const resetDemo = runDemo(api);
$('reset').addEventListener('click', () => { timers.forEach(clearTimeout); timers.clear(); resetDemo(); tasks = base.map((task) => ({ ...task })); render(); trigger.disabled = false; say(initialHint); $('reset').focus({ preventScroll: true }); });
document.body.dataset.theme = new URLSearchParams(location.search).get('theme') === 'dark' ? 'dark' : 'light';
document.body.dataset.ready = 'true';
document.body.dataset.demo = kind;
})("dialog","输入任务标题,提交后立即回到列表。",function dialogDemo({ $, trigger, say, openModal, closeModal, getTasks, setTasks }) {
const modal = $('dialog'); const form = $('task-form'); const input = $('title-input'); let submitting = false; let nextId = 4;
const errorLabel = document.createElement('label'); errorLabel.className = 'failure-option';
const errorSwitch = document.createElement('input'); errorSwitch.type = 'checkbox'; errorSwitch.dataset.testid = 'simulate-error'; errorSwitch.name = 'simulate-error';
errorLabel.append(errorSwitch, document.createTextNode('模拟提交失败(体验输入保留)')); form.insertBefore(errorLabel, $('form-error'));
trigger.setAttribute('aria-haspopup', 'dialog');
const resetForm = () => { form.reset(); $('form-error').hidden = true; $('form-error').textContent = '请填写任务标题。'; input.removeAttribute('aria-invalid'); submitting = false; $('confirm').disabled = false; };
trigger.addEventListener('click', () => { resetForm(); openModal(modal); input.focus(); say('输入一个任务标题,创建你的下一步。'); });
const cancel = () => { closeModal(modal); resetForm(); say('已关闭新建窗口。'); };
$('close').addEventListener('click', cancel); $('cancel').addEventListener('click', cancel); modal.addEventListener('cancel', (event) => { event.preventDefault(); cancel(); });
input.addEventListener('input', () => { $('form-error').hidden = true; input.removeAttribute('aria-invalid'); });
form.addEventListener('submit', (event) => {
event.preventDefault(); if (submitting) return;
const title = input.value.trim(); if (!title) { $('form-error').textContent = '请填写任务标题。'; $('form-error').hidden = false; input.setAttribute('aria-invalid', 'true'); input.focus(); return; }
submitting = true; $('confirm').disabled = true;
if (errorSwitch.checked) { $('form-error').textContent = '提交未成功,内容已保留。取消勾选后可重试。'; $('form-error').hidden = false; submitting = false; $('confirm').disabled = false; say('本次提交失败,标题与说明已保留。'); return; }
const description = $('description-input').value.trim();
setTasks([...getTasks(), { id: nextId++, title, meta: description || '新建任务 · 今天', owner: '我' }]); closeModal(modal); say('已创建任务:' + title);
});
return () => { closeModal(modal); resetForm(); nextId = 4; };
});</script></body></html>
8. Tour:带新用户走一遍关键路径
适合首次使用时介绍任务区、筛选和新增入口。每一步都要回答“这里能做什么”,也要允许熟悉产品的人随时退出。
给 AI 的提示词:
为任务管理页制作四步 Tour,每步定位一个真实存在的界面元素,显示步骤数、说明、上一步、下一步与跳过入口,结束时可完成退出。支持 Esc 和键盘操作;窗口变窄、滚动后重新检查定位,目标不存在时安全退出。
**亲手验收:**前进、后退、中途退出,再重新开始。手机上说明卡不能挡住退出入口。引导不应擅自创建任务,也不能为了展示步骤替用户执行删除。
Tour · 漫游引导
跟随四步引导,也可以随时跳过。
可直接操作。数据只在此演示区域临时变化,点击“重新开始”恢复初始状态。

查看完整可运行源码
与上方演示使用相同源码,无额外依赖;保存为 .html 文件即可打开。
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><meta name="color-scheme" content="light dark"><title>Tour · 第一次,也能上手 — 交互演示</title><style>
:root{color-scheme:light;--bg:#f5f5f2;--paper:#fff;--ink:#242424;--muted:#85857f;--line:#e9e9e3;--soft:#f8f8f5;--accent:#df642b;--tint:#fff2e9;--shadow:0 12px 45px #2221;--overlay:#16161655;font-family:Arial,"PingFang SC","Microsoft YaHei",sans-serif}*{box-sizing:border-box}html,body{margin:0;min-width:0;min-height:100%;width:100%}body{background:var(--bg);color:var(--ink);font-size:12px;line-height:1.5}body[data-theme=dark]{color-scheme:dark;--bg:#171818;--paper:#222323;--ink:#ededeb;--muted:#aaa9a2;--line:#3b3b37;--soft:#292a28;--accent:#f39158;--tint:#413025;--shadow:0 12px 45px #0005;--overlay:#0009}button,input,textarea{font:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.45}button,input,textarea{outline:none}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button{border:0}button:active{transform:translateY(1px)}[hidden]{display:none!important}.stage{width:min(100%,680px);height:480px;margin:0 auto;padding:27px 30px 18px;position:relative;overflow:hidden;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:7px;font-size:10px;letter-spacing:1.5px;font-weight:700;color:var(--muted);margin-bottom:21px}.brand-mark{height:8px;width:8px;background:var(--accent)}.workspace{background:var(--paper);border:1px solid var(--line);border-radius:11px;box-shadow:0 3px 18px #22200008;padding:20px 20px 0;flex:1;min-height:0;display:flex;flex-direction:column}.workspace-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:19px}h1{font-size:21px;letter-spacing:-.8px;margin:0;font-weight:650}h2{font-size:18px;line-height:1.4;margin:10px 0 8px;font-weight:650;letter-spacing:-.4px}h3{font-size:12px;margin:20px 0 8px}p{margin:0}.subtitle{font-size:10px;color:var(--muted);margin-top:4px}.primary,.secondary{border-radius:5px;padding:8px 11px;white-space:nowrap;font-weight:600;font-size:11px;line-height:1.3}.primary{color:var(--paper);background:var(--ink)}.primary:hover{opacity:.85}.secondary{color:var(--ink);background:var(--paper);border:1px solid var(--line)}.secondary:hover{background:var(--soft)}.primary.accent-button{color:var(--accent);background:var(--tint);border:1px solid color-mix(in srgb,var(--accent) 15%,transparent)}.toolbar{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding:0 0 11px}.tab{font-size:10px;font-weight:600}.count{margin-left:6px;padding:1px 5px;border-radius:4px;background:var(--soft);color:var(--muted);font-size:9px}.toolbar-note{font-size:9px;color:var(--muted)}.task-list{padding:0;margin:0;list-style:none;overflow:auto;min-height:0;flex:1}.task{min-height:55px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:11px}.task:last-child{border-bottom:0}.check{width:14px;height:14px;flex:0 0 14px;border:1px solid #d6d6ce;border-radius:4px;font-size:10px;display:grid;place-items:center}.task.done .check{border-color:var(--accent);color:var(--accent);background:var(--tint)}.task.done .task-title{text-decoration:line-through;color:var(--muted)}.task-main{min-width:0;flex:1}.task-title{font-size:11px;font-weight:550;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.task-meta{font-size:9px;color:var(--muted);margin-top:3px}.priority{font-size:9px;padding:3px 5px;border-radius:4px;color:var(--accent);background:var(--tint);white-space:nowrap}.avatar{width:23px;height:23px;display:grid;place-items:center;flex-shrink:0;border-radius:50%;background:var(--soft);border:1px solid var(--line);color:var(--muted);font-size:9px}.workspace-foot{border-top:1px solid var(--line);color:var(--muted);padding:11px 0;font-size:9px;display:flex;align-items:center;gap:6px}.live-dot{width:4px;height:4px;border-radius:50%;background:var(--accent)}.demo-foot{display:flex;align-items:flex-start;gap:15px;justify-content:space-between;padding:15px 1px 0;min-height:53px}.demo-label{font-size:9px;color:var(--muted);letter-spacing:.6px;font-weight:600}.status{font-size:10px;color:var(--muted);margin-top:4px;min-height:15px}.text-button{color:var(--accent);background:none;padding:3px 4px;font-weight:600;font-size:11px}.reset{font-size:10px;color:var(--muted);border-bottom:1px solid var(--line);background:none;padding:3px 0;white-space:nowrap}.floating{position:fixed;z-index:10;background:var(--paper);border:1px solid var(--line);border-radius:9px;box-shadow:var(--shadow)}.toast{bottom:75px;right:max(22px,calc((100vw - 580px)/2));display:flex;align-items:center;gap:11px;padding:12px 15px;max-width:calc(100vw - 32px);animation:appear .18s ease-out;font-size:11px}.success-mark{color:var(--accent);font-size:15px}.hover-card{width:275px;padding:18px}.profile{display:flex;align-items:center;gap:10px;margin-bottom:13px}.profile strong{font-size:13px}.profile p{font-size:9px;color:var(--muted);margin-top:3px}.large{width:35px;height:35px;background:var(--tint);color:var(--accent);border:0;font-size:13px}.online{margin-left:auto;width:6px;height:6px;border-radius:50%;background:var(--accent)}.hover-card>p{color:var(--muted);font-size:10px}.profile-stats{display:flex;justify-content:space-between;border-top:1px solid var(--line);padding-top:12px;margin-top:15px;font-size:9px;color:var(--muted)}.profile-stats strong{display:block;color:var(--ink);font-size:16px;font-weight:500;margin-bottom:3px}.popconfirm{width:270px;padding:18px}.eyebrow{font-size:9px;font-weight:600;color:var(--accent);letter-spacing:1px}.popconfirm p,.center-dialog>p,.tour>p{font-size:11px;color:var(--muted);line-height:1.8}.button-row{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}.panel-top{display:flex;justify-content:space-between;align-items:center;gap:10px}.icon-button{width:27px;height:27px;border-radius:5px;background:var(--soft);color:var(--muted);font-size:20px;line-height:1}.icon-button:hover{color:var(--ink)}dialog{color:var(--ink);background:var(--paper);border:1px solid var(--line);box-shadow:var(--shadow);padding:24px;max-width:calc(100vw - 32px);max-height:calc(100dvh - 32px)}dialog::backdrop{background:var(--overlay);backdrop-filter:blur(2px)}.drawer{position:fixed;inset:16px 16px 16px auto;margin:0;width:320px;height:calc(100dvh - 32px);border-radius:10px;animation:slide-in .2s ease-out;overflow:auto}.muted{color:var(--muted);font-size:11px}.detail-grid{display:grid;grid-template-columns:80px 1fr;gap:12px 8px;margin-top:25px;padding-bottom:20px;border-bottom:1px solid var(--line);font-size:10px}.detail-grid>span{color:var(--muted)}.detail-grid strong{font-weight:500}.body-copy{font-size:11px;line-height:1.9;color:var(--muted)}.subtasks{background:var(--soft);border-radius:5px;padding:10px;margin-top:14px;font-size:10px;color:var(--muted);line-height:2}.panel-actions{display:flex;justify-content:flex-end;margin-top:20px}.center-dialog{width:355px;border-radius:11px;animation:appear .18s ease-out}.form-dialog{width:360px;padding:20px}.form-dialog h2{margin:8px 0 16px}.form-dialog label{display:block;font-size:10px;font-weight:600;margin:12px 0 6px}.form-dialog .failure-option{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:9px;font-weight:400}.failure-option input{width:12px;height:12px;padding:0;accent-color:var(--accent)}.optional{font-weight:400;color:var(--muted);margin-left:5px}input,textarea{background:var(--paper);color:var(--ink);border:1px solid var(--line);border-radius:5px;width:100%;padding:9px 10px;font-size:11px;resize:vertical}input::placeholder,textarea::placeholder{color:var(--muted)}.accent{color:var(--accent)}.form-error{font-size:10px;color:var(--accent);margin-top:7px}.tooltip{z-index:20;position:fixed;width:max-content;max-width:calc(100vw - 32px);padding:8px 11px;border-radius:5px;background:var(--ink);color:var(--paper);font-size:10px;pointer-events:none;box-shadow:var(--shadow)}.tooltip-arrow{position:absolute;top:-4px;left:50%;width:8px;height:8px;transform:rotate(45deg);background:var(--ink)}.empty{text-align:center;color:var(--muted);padding:40px 10px;list-style:none;font-size:11px}.tour-blocker{position:fixed;inset:0;z-index:30}.tour-spotlight{position:fixed;z-index:31;border:2px solid var(--accent);border-radius:7px;box-shadow:0 0 0 9999px var(--overlay);pointer-events:none;transition:all .18s}.tour{z-index:32;width:275px;padding:16px}.tour-actions{display:flex;align-items:center;justify-content:space-between;margin-top:16px;gap:8px}.tour-actions .button-row{margin:0;gap:5px}.tour-actions .primary,.tour-actions .secondary{padding:7px 9px;font-size:10px}@keyframes appear{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@keyframes slide-in{from{opacity:.5;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@media(max-width:440px){.stage{padding:21px 16px 15px}.workspace{padding:17px 14px 0}.brand{margin-bottom:18px}.workspace-head{gap:7px}h1{font-size:19px}.workspace-head>.primary{font-size:10px;padding:8px}.toolbar-note{display:none}.task{gap:8px}.task-title{font-size:10px}.avatar{width:21px;height:21px}.priority{font-size:8px}.demo-foot{gap:8px}.status{font-size:9px}.drawer{width:calc(100vw - 32px)}.toast{right:16px}.tour{width:260px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
</style></head>
<body><main class="stage"><div class="brand"><span class="brand-mark"></span> SMALL STEPS / 任务工作台</div><section class="workspace" aria-label="每日任务工作台"><header class="workspace-head"><div data-tour="heading"><h1>今天,做好这几件事。</h1><p class="subtitle">把注意力留给真正重要的事情</p></div><button class="primary " data-testid="trigger">开始引导</button></header><div class="toolbar" data-tour="toolbar"><span class="tab">全部任务<span class="count" data-testid="task-count">3</span></span><span class="toolbar-note">按优先级排列 ↕</span></div><ul class="task-list" data-testid="task-list" aria-label="任务列表"></ul><div class="workspace-foot"><span class="live-dot"></span> 每一步,都算数。<span style="margin-left:auto">今日计划 / 03</span></div></section><footer class="demo-foot"><div><div class="demo-label">08 / TOUR <span style="margin-left:7px;font-weight:400">第一次,也能上手</span></div><p class="status" data-testid="status" role="status" aria-live="polite">4 个小步骤,认识这个任务工作台。</p></div><button class="reset" data-testid="reset">↺ 重置</button></footer></main><div class="tour-blocker" hidden></div><div class="tour-spotlight" hidden></div><section class="tour floating" data-testid="tour" role="dialog" aria-modal="true" aria-labelledby="tour-title" hidden><div class="panel-top"><span class="eyebrow" data-testid="tour-progress"></span><button class="icon-button" data-testid="close" aria-label="关闭引导">×</button></div><h2 id="tour-title" data-testid="tour-title"></h2><p data-testid="tour-description"></p><div class="tour-actions"><button class="text-button" data-testid="skip">跳过</button><div class="button-row"><button class="secondary" data-testid="prev">上一步</button><button class="primary" data-testid="next">下一步</button></div></div></section><script>(function sharedRuntime(kind, initialHint, runDemo) {
'use strict';
const $ = (id) => document.querySelector('[data-testid="' + id + '"]');
const trigger = $('trigger');
const status = $('status');
const list = $('task-list');
const base = [{ id: 1, title: '确认首页视觉稿', meta: '设计迭代 · 今天', owner: '林', priority: true }, { id: 2, title: '整理用户访谈记录', meta: '用户研究 · 明天', owner: '周' }, { id: 3, title: '准备周五的产品分享', meta: '团队协作 · 周五', owner: '陈' }];
let tasks = base.map((task) => ({ ...task }));
const timers = new Set();
const later = (callback, delay) => { const timer = setTimeout(() => { timers.delete(timer); callback(); }, delay); timers.add(timer); return timer; };
const stop = (timer) => { clearTimeout(timer); timers.delete(timer); };
const say = (text) => { status.textContent = text; };
const render = () => {
list.replaceChildren();
tasks.forEach((task) => {
const row = document.createElement('li'); row.className = 'task' + (task.done ? ' done' : ''); row.dataset.testid = 'task-row-' + task.id; row.dataset.taskId = String(task.id);
const check = document.createElement('span'); check.className = 'check'; check.textContent = task.done ? '✓' : ''; check.setAttribute('aria-hidden', 'true');
const main = document.createElement('div'); main.className = 'task-main';
const title = document.createElement('div'); title.className = 'task-title'; title.textContent = task.title;
const meta = document.createElement('div'); meta.className = 'task-meta'; meta.textContent = task.meta;
main.append(title, meta); row.append(check, main);
if (task.priority) { const badge = document.createElement('span'); badge.className = 'priority'; badge.textContent = '高优先级'; row.append(badge); }
const owner = document.createElement('span'); owner.className = 'avatar'; owner.textContent = task.owner; owner.setAttribute('aria-label', '负责人 ' + task.owner); row.append(owner); list.append(row);
});
if (!tasks.length) { const empty = document.createElement('li'); empty.className = 'empty'; empty.dataset.testid = 'empty'; empty.textContent = '任务都处理好了,留一点时间给自己。'; list.append(empty); }
$('task-count').textContent = String(tasks.length);
};
const place = (element, anchor = trigger, gap = 10) => {
const a = anchor.getBoundingClientRect(); const r = element.getBoundingClientRect();
element.style.left = Math.max(12, Math.min(a.right - r.width, innerWidth - r.width - 12)) + 'px';
element.style.top = Math.max(12, Math.min(a.bottom + gap, innerHeight - r.height - 12)) + 'px';
};
const openModal = (modal) => { if (!modal.open) modal.showModal(); trigger.setAttribute('aria-expanded', 'true'); };
const closeModal = (modal) => { if (modal.open) modal.close(); trigger.setAttribute('aria-expanded', 'false'); trigger.focus({ preventScroll: true }); };
const api = { $, trigger, list, later, stop, say, render, place, openModal, closeModal, getTasks: () => tasks, setTasks: (next) => { tasks = next; render(); } };
render();
const resetDemo = runDemo(api);
$('reset').addEventListener('click', () => { timers.forEach(clearTimeout); timers.clear(); resetDemo(); tasks = base.map((task) => ({ ...task })); render(); trigger.disabled = false; say(initialHint); $('reset').focus({ preventScroll: true }); });
document.body.dataset.theme = new URLSearchParams(location.search).get('theme') === 'dark' ? 'dark' : 'light';
document.body.dataset.ready = 'true';
document.body.dataset.demo = kind;
})("tour","4 个小步骤,认识这个任务工作台。",function tourDemo({ $, trigger, say }) {
const panel = $('tour'); const blocker = document.querySelector('.tour-blocker'); const spotlight = document.querySelector('.tour-spotlight'); let index = 0; let previousFocus = null;
const steps = [
{ target: '[data-tour="heading"]', title: '你的每日工作台', description: '从这里开始,专注今天最重要的事情。' },
{ target: '[data-tour="toolbar"]', title: '所有任务,一眼掌握', description: '任务总数会随创建和删除实时更新。' },
{ target: '[data-testid="task-row-1"]', title: '先处理高优先级', description: '橙色标签,提醒你把时间留给重要的事。' },
{ target: '[data-testid="task-row-2"]', title: '协作信息,就在手边', description: '任务的负责人和时间,让团队行动更清楚。' },
];
const position = () => {
if (panel.hidden) return false;
const target = document.querySelector(steps[index].target);
if (!target) { close('当前引导目标已变化,请重新开始。'); return false; }
const rect = target.getBoundingClientRect();
spotlight.style.left = Math.max(4, rect.left - 5) + 'px'; spotlight.style.top = Math.max(4, rect.top - 5) + 'px'; spotlight.style.width = Math.min(innerWidth - 8, rect.width + 10) + 'px'; spotlight.style.height = rect.height + 10 + 'px';
const size = panel.getBoundingClientRect();
panel.style.left = Math.max(12, Math.min(rect.left, innerWidth - size.width - 12)) + 'px';
const below = rect.bottom + 13; const above = rect.top - size.height - 13;
panel.style.top = Math.max(12, Math.min(below + size.height <= innerHeight - 12 ? below : above, innerHeight - size.height - 12)) + 'px';
return true;
};
const showStep = () => { if (panel.hidden) return false; const step = steps[index]; panel.dataset.step = String(index + 1); $('tour-progress').textContent = '快速上手 / 0' + (index + 1) + ' · 04'; $('tour-title').textContent = step.title; $('tour-description').textContent = step.description; $('prev').disabled = index === 0; $('next').textContent = index === 3 ? '完成' : '下一步'; if (!position()) return false; say('引导第 ' + (index + 1) + ' 步,共 4 步。'); return true; };
const close = (message = '已退出引导,随时可以重新开始。') => { panel.hidden = true; blocker.hidden = true; spotlight.hidden = true; trigger.setAttribute('aria-expanded', 'false'); if (previousFocus && previousFocus.isConnected) previousFocus.focus({ preventScroll: true }); say(message); };
trigger.setAttribute('aria-haspopup', 'dialog'); trigger.setAttribute('aria-expanded', 'false');
trigger.addEventListener('click', () => { previousFocus = document.activeElement; index = 0; panel.hidden = false; blocker.hidden = false; spotlight.hidden = false; trigger.setAttribute('aria-expanded', 'true'); if (showStep()) $('next').focus(); });
$('next').addEventListener('click', () => { if (index < 3) { index++; showStep(); } else close('引导完成。现在开始安排你的一天吧。'); });
$('prev').addEventListener('click', () => { if (index > 0) { index--; if (showStep() && index === 0) $('next').focus(); } });
$('skip').addEventListener('click', () => close()); $('close').addEventListener('click', () => close());
document.addEventListener('keydown', (event) => {
if (panel.hidden) return;
if (event.key === 'Escape') { event.preventDefault(); close(); }
if (event.key === 'Tab') { const controls = Array.from(panel.querySelectorAll('button:not(:disabled)')); const first = controls[0]; const last = controls[controls.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } }
});
addEventListener('resize', position); addEventListener('scroll', position, true);
return () => { close(); index = 0; previousFocus = null; };
});</script></body></html>
把“弹个窗”改成可验收的需求
可以沿用这个句式:谁在什么情况下触发 → 展示什么 → 用户能做什么 → 如何退出 → 失败如何恢复 → 手机和键盘怎么用。 每次先跑成功路径,再跑取消、失败和窄屏路径;源码区提供的完整 HTML 就是可修改的起点。
如果你还在从零搭项目,可以接着看从想法到可验证成果的 Vibe Coding 工作流,把这些交互要求放进每轮小步实现的验收条件里。
本文的场景与八类顺序参考阿甘探 AI 的快手视频;实现边界结合上文链接的 Radix Toast、Hover Card 与 W3C Dialog、Tooltip 官方文档整理。演示代码、AI 提示词与补充验证方法由本站编写。