最糟糕的 UX 反模式及修复方法
生产环境中仍在大量出现的糟糕 UX 反模式,附暗黑模式、失效开关和无障碍问题的前后代码修复示例。

上周我想在某大型航空公司的网站上调整 Cookie 偏好设置。「全部接受」按钮是醒目的亮蓝色,想忽略都难。「管理偏好」链接呢?灰色文字、11px 字号,藏在一大段法律条款下面。好不容易找到了,结果页面上有 47 个单独的开关,全部默认开启,连个「全部拒绝」按钮的影子都没有。我坐在那里足足点了一分钟开关,最后放弃了,直接手动清除了 Cookie。
这不是个例,而是常态。糟糕的 UX 不只是烦人,而是充满敌意。最糟心的是,大多数反模式都有极其简单的修复方案,实现起来比那些为了暗黑模式而绕来绕去的设计还省时间。我做 Web 应用已经十多年了,至今都想不明白我们为什么还在一直上线这些东西。所以,咱们来聊聊最恶劣的几种,以及怎么把它们干掉。
把用户当韭菜的暗黑模式
暗黑模式不是意外。总有人坐在会议室里,盯着转化率数据,然后判定坑骗用户是一种可以接受的商业策略。正因为是刻意为之,它们才格外让人火大。
道德绑架式按钮:把愧疚感做成 UI 元素
这种设计你一定见过。弹窗请你订阅邮件,同意按钮写着「好啊,我要省钱!」,拒绝按钮写着「不了谢谢,我宁愿原价买」。这就是道德绑架式按钮(confirmshaming),电商站、SaaS 的新手引导甚至一些开发者工具里都能看到。它可能会打动极少数用户,却会让其他所有人都反感。
修复方法简单到让人有点尴尬:两个选项都用中性的措辞。
<!-- BEFORE: Manipulative confirmshaming -->
<div class="modal">
<p>Join 50,000 smart developers!</p>
<button class="btn-primary">Yes, sign me up!</button>
<button class="btn-link text-sm">
No thanks, I don't care about my career
</button>
</div>
<!-- AFTER: Respectful, neutral options -->
<div class="modal">
<p>Get weekly frontend tips — no spam, unsubscribe anytime.</p>
<button class="btn-primary">Subscribe</button>
<button class="btn-secondary">No thanks</button>
</div>
注意修复后的版本还去掉了夸大的社会认同文案,并且明确承诺不会发垃圾邮件。尊重用户带来的信任,远比操纵用户来得持久。你的长期转化率会感谢你的。
捕鼠笼陷阱:一键注册,七步取消
注册只需 30 秒,取消却要依次点击:设置 > 账户 > 订阅 > 管理套餐 > 取消套餐 > 告诉我们原因 > 你确定吗 > 联系挽留专员 > 确认取消。有些服务至今还要求打电话。都 2026 年了。开通订阅只需一次点击,取消却要过五关斩六将。
你们的留存数据怎么说我不管。被困住的用户不是忠实客户,而是随时准备发起拒付和给一星差评的「人质」。让取消订阅和注册一样简单,就这么定了。
- 把取消选项放在用户预期的账户设置里
- 最多两步:先是「确定要取消吗?」,然后是「完成,账户已取消」
- 别把取消按钮藏在「联系客服」链接后面
- 如果你要提供挽留优惠,可以,但别把它变成流程中必须经过的一步
- 发一封带重新激活链接的确认邮件,别让取消显得无法挽回
含糊不清的开关和令人困惑的 UI 控件
来个小实验:打开手机设置,数一数有多少个开关你一眼看不出是开着还是关着。我等你。
在代码评审中,我遇到的最常见的 UI 问题之一就是含糊的开关。开关应该只传达一件事:当前的状态。它是开还是关?仅此而已。但开发者总是做出两种状态看起来几乎一模一样的开关,或者颜色含义模棱两可的开关,又或者开关显示的是接下来会发生什么,而不是当前正在发生什么。
/* BEFORE: Both states look nearly identical */
.toggle {
width: 48px;
height: 24px;
background: #d1d5db;
border-radius: 12px;
cursor: pointer;
}
.toggle.active {
background: #9ca3af; /* barely different from inactive */
}
/* AFTER: Unmistakable state difference + accessible */
.toggle {
width: 48px;
height: 24px;
background: #d1d5db;
border-radius: 12px;
cursor: pointer;
position: relative;
}
.toggle[aria-checked="true"] {
background: #2563eb; /* strong contrast from inactive */
}
.toggle .label {
font-size: 10px;
font-weight: 600;
color: white;
}
.toggle[aria-checked="true"] .label::after { content: "ON"; }
.toggle[aria-checked="false"] .label::after { content: "OFF"; }
要让开关不让人困惑,记住三条规则:两种状态使用明显不同的颜色(并保证足够的对比度,让色盲用户也能分辨),在开关内部或旁边配上文字标签,并正确设置 aria-checked 属性,让屏幕阅读器能播报当前状态。如果只靠颜色来区分,那你同时在用户体验和无障碍两方面都失败了。
别再重复造原生表单控件的轮子
我审过很多前端 PR,有一种模式让我特别抓狂:自定义下拉框破坏了键盘导航。开发者花两天时间,用一堆 div 和点击事件从零搭了一个花哨的选择菜单。演示时效果很好。结果用户用 Tab 键切换表单时,碰到这个自定义下拉框,什么反应都没有。不支持方向键,没有输入检索,密码管理器也用不了,移动端还崩了。
其实原生的 <select> 元素,或者经过充分测试的库(比如 Radix UI 或 Headless UI),这些问题都帮你处理好了。HTML 的 popover API 和 <selectlist> 元素现在也有了不错的浏览器支持。用它们就对了。用户不在乎你的下拉框有没有炫酷的动画,他们只关心它能不能用。
把真实用户拒之门外的无障碍反模式
无障碍不是锦上添花,也不是可以留到以后某个迭代再加的功能。全球有超过十亿人生活在某种形式的残障之中,而 WebAIM Million 报告一直显示,前一百万个网站中有 96% 以上存在可检测到的 WCAG 不合规问题。这些都不是什么冷门的边缘情况,而是按钮失效、标签缺失,以及没有 alt 文本的图片。
以下是我最常看到的问题:
- 图片没有 alt 文本,屏幕阅读器只会念一句「图片」就跳过
- 对比度低于 4.5:1,低视力用户根本看不清文字
- 无法使用键盘导航,如果你的应用不能用 Tab 键逐个切换,键盘和辅助开关设备的用户就被挡在门外了
- 弹窗中的焦点陷阱,用户打开对话框后,没有鼠标就无法退出
- 表单缺少标签,屏幕阅读器不知道输入框是干什么用的
- 自动播放的视频没有暂停按钮,对前庭功能障碍的用户来说非常眩晕
最快见效的办法,是在 CI 流水线中加入自动化无障碍检查。它不可能发现所有问题,自动化工具大概只能找出 30% 到 40% 的问题,但能在上线前拦住那些明显的毛病。
// BEFORE: No accessibility testing at all
test('homepage loads', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});
// AFTER: Accessibility violations fail the build
import AxeBuilder from '@axe-core/playwright';
test('homepage loads and is accessible', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
const a11yResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(a11yResults.violations).toEqual([]);
});
设置只需五分钟,每个 PR 都会运行。它能自动检测缺失的 alt 文本、错误的 ARIA 角色、对比度不足,以及其他数十种问题。没有理由不做。
认知过载:堪称噩梦的设置页面
人的工作记忆一次只能容纳大约四到七个项目。这不是建议,而是硬性的认知上限。当你的界面把 200 个选项一股脑堆在一个页面上,没有任何层级结构时,你并没有给用户控制权,而是在制造选择困难症。
我曾经数过我们公司在用的一个项目管理工具的设置项:347 个选项,全在一个页面上。没有搜索,除了「通用」和「高级」这种含糊的分类之外也没有其他分组。团队里有一半人从来没改过任何设置,因为那个页面实在太让人窒息了,他们一看到就直接关掉了。
解决办法是渐进式展示。只展示用户真正会修改的五个设置。其他所有选项都收进标注清晰的可展开区域里。加上搜索框。看在老天的份上,提供合理的默认值,让大多数用户根本不需要打开设置页面。
如果你的设置页面需要单独的文档来解释,那问题就出在设置页面本身。
通知泛滥,让用户学会对一切视而不见
每个事件都弹通知,比如有队友加入了频道、有人用表情回应了消息、部署完成了、日历事件 30 分钟后开始。用户很快就学会了无视所有通知。你的通知系统变成了白噪音。那条真正重要的关键警报呢?早就被埋在 47 条未读通知底下了。
答案是保守的默认值。新用户应该只收到关键通知。把不紧急的内容合并成每日摘要。而且务必(真的,一定要)让用户直接在通知本身上静音或自定义,而不是让他们在三次点击之外的某个隐藏偏好页面里去找。
慢就是坏:把性能当作 UX 问题来对待
按钮点了两秒才有反应,这不叫慢,这叫坏了。用户不会想「哦,服务器正在处理我的请求」。他们想的是「我的点击生效了吗?」,然后就再点一次。于是你就得到了重复提交、混乱的状态,以及一个不再信任你应用的用户。
超过 100 毫秒,用户就会觉得卡顿;超过一秒,用户的思路就被打断了。可我们还是一直在上线动辄几兆的 JavaScript 包、阻塞渲染的第三方脚本,以及让人点错元素的布局偏移。解决办法并不复杂,只是需要真正在乎它。
// BEFORE: User clicks, waits 2 seconds, nothing happens
async function handleLike(postId: string) {
const result = await fetch(`/api/posts/${postId}/like`, {
method: 'POST',
});
if (result.ok) {
setLikeCount(prev => prev + 1); // UI updates after network round-trip
}
}
// AFTER: Optimistic update — UI responds instantly
function handleLike(postId: string) {
// Update UI immediately
setLikeCount(prev => prev + 1);
// Sync with server in background
startTransition(async () => {
const result = await fetch(`/api/posts/${postId}/like`, {
method: 'POST',
});
if (!result.ok) {
// Rollback only on failure
setLikeCount(prev => prev - 1);
toast.error('Something went wrong. Try again.');
}
});
}
乐观更新,用骨架屏代替加载转圈,对非关键的 JavaScript 做懒加载,并且把 Core Web Vitals 当作真正的指标来监控,而不是事后才想起来的东西。这些都不是什么高级技巧,而是最基本的要求。
死不掉的移动端 UX 反模式
移动端有自己专属的一套 UX 罪状,主要是因为开发者总是用一台全新的 iPhone 测试,然后就收工了。
需要精准到像做手术一样才能点中的触控目标
苹果的指南规定触控目标至少为 44x44 CSS 像素,WCAG 2.2 也是这么要求的。但我还是经常看到弹窗上只有 20 像素的关闭按钮、页脚里挤在一起的小链接,以及在手机上几乎点不中的图标按钮。对于运动功能障碍的用户来说,情况更糟。
/* BEFORE: Tiny, frustrating touch targets */
.close-btn {
width: 20px;
height: 20px;
font-size: 12px;
}
.footer-links a {
padding: 2px 4px;
font-size: 11px;
}
/* AFTER: Actually tappable elements */
.close-btn {
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.footer-links a {
padding: 12px 8px;
font-size: 14px;
/* spacing between adjacent targets */
margin: 4px 0;
}
阻挡内容的全屏插屏广告
你点开一条搜索结果,页面加载出来,还没等你读到一个字,一个全屏弹窗就要你留下邮箱。你连内容都还没看到,凭什么要订阅?
自 2017 年起,Google 就开始在搜索排名中惩罚侵入式插屏广告。但它们之所以至今还在,是因为总有人盯着仪表盘,看到邮件采集率 2% 就觉得大功告成,却无视了它带来的 40% 跳出率。改用页内横幅或底部浮层吧。等用户真正读过你的内容之后,再请求任何东西。一个读完三篇文章的读者,会心甘情愿地订阅。而一个被打断了三次的读者,只会离开,再也不回来。
如何建立 UX 质量文化(而不只是逐个修 Bug)
逐个修复反模式是必要的,但远远不够。如果你的流程不断地产出这些问题,那你就需要一个更好的流程。以下是真正有效的做法:
- 在 CI 流水线中加入 axe-core,对每个 PR 做自动化无障碍检查
- 把 UX 评审纳入代码评审,而不是另设一个只有设计师参与的流程
- 在发布重大功能前找 5 个真实用户测试,5 个用户就能发现约 85% 的可用性问题
- 跟踪任务完成率和错误率,而不只是页面浏览量和停留时长
- 使用带有预制无障碍组件的设计系统,让开发者不必每次都从零解决同样的交互问题
- 拼命地用自家产品(吃自己的狗粮),当写设置页面的开发者每天都得用它时,问题很快就会被修好
我做过的最好的一次 UX 改进,起因是我试着在手机上走一遍我们自家的结账流程,然后在晚上 11 点对着产品群疯狂吐槽。
本文中的每一种反模式都有直接的修复方法。它们都不需要尖端技术,也不需要大规模重新设计。它们需要的只是真正上心。今天就修一个,这周就做一次无障碍审计,这个月就找一个真实用户测试。好的 UX 不在于华丽的大动作,而在于始终如一地把对用户的尊重放在短期指标之上。人们能忍受的软件和人们真正喜爱的软件之间的距离,比你想象的要近。它只是由一千个小决定组成,而每个决定都做得足够好。


