Last week I tried to adjust the cookie preferences on a major airline's website. The 'Accept All' button was a bright blue, impossible to miss. The 'Manage Preferences' link? Gray text, 11px font, tucked under a paragraph of legalese. When I finally found it, I landed on a page with 47 individual toggle switches — all defaulted to on — with no 'Reject All' button in sight. I sat there for a full minute clicking toggles before I gave up and just cleared my cookies manually.
This isn't a one-off. It's the norm. Bad UX isn't just annoying — it's hostile. And the worst part? Most of these anti-patterns have dead-simple fixes that take less time to implement than the dark pattern gymnastics they're replacing. I've been building web apps for over a decade, and I'm genuinely baffled that we keep shipping this stuff. So let's talk about the worst offenders and how to kill them.
Dark Patterns That Treat Users Like Marks
Dark patterns aren't accidents. Someone sat in a meeting, looked at conversion metrics, and decided that tricking users was an acceptable business strategy. That's what makes them so infuriating — they're deliberate.
Confirmshaming: Guilt as a UI Element
You've seen these. A modal pops up asking you to subscribe to a newsletter. The accept button says 'Yes, I want to save money!' The decline button says 'No thanks, I prefer paying full price.' This is confirmshaming, and it's everywhere — e-commerce sites, SaaS onboarding flows, even some developer tools. It works on a tiny percentage of users and alienates everyone else.
The fix is embarrassingly simple: use neutral language for both options.
<!-- 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>
Notice the after version also drops the inflated social proof and adds a clear promise about spam. Respect builds more trust than manipulation. Your long-term conversion will thank you.
The Roach Motel: One-Click Signup, Seven-Step Cancellation
Signing up takes 30 seconds. Canceling requires you to navigate to Settings > Account > Subscription > Manage Plan > Cancel Plan > Tell Us Why > Are You Sure > Talk to Retention > Actually Cancel. Some services still require a phone call. In 2026. To cancel a subscription you started with a single click.
I don't care what your retention metrics say. Users who are trapped aren't loyal customers — they're hostages generating chargebacks and one-star reviews. Make cancellation exactly as easy as signup.
- Put the cancel option in Account Settings, where people expect it
- Two steps max: 'Are you sure?' then 'Done, your account is canceled'
- Don't hide the cancel button behind a 'Contact Support' link
- If you offer a retention discount, fine — but don't make it a required step in the flow
- Send a confirmation email with a reactivation link instead of making cancellation feel irreversible
Broken Toggle Switches and Confusing UI Controls
Here's a fun experiment: go to your phone's settings and count how many toggles you can't immediately tell are on or off. I'll wait.
The ambiguous toggle is one of the most common UI failures I encounter in code reviews. A toggle should communicate exactly one thing: the current state. Is this on, or is this off? That's it. But developers keep shipping toggles where both states look almost identical, or where the color coding is ambiguous, or where the toggle shows what will happen next instead of what's happening now.
/* 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"; }
Three rules for toggles that don't confuse people: distinct colors for each state (with enough contrast to work for colorblind users), a text label inside or adjacent to the toggle, and proper aria-checked attributes so screen readers can announce the state. If you're relying on color alone, you've failed both UX and accessibility in one move.
Stop Reinventing Native Form Controls
I review a lot of frontend PRs, and one pattern drives me up the wall: custom-built dropdowns that break keyboard navigation. A developer spends two days building a fancy select menu from scratch with a bunch of divs and click handlers. It looks great in the demo. Then a user tries to tab through a form, hits the custom dropdown, and nothing happens. No arrow key support. No type-ahead search. Doesn't work with password managers. Breaks on mobile.
Meanwhile, the native
Accessibility Anti-Patterns That Exclude Real Users
Accessibility isn't a nice-to-have. It isn't a feature you add in a future sprint. Over a billion people worldwide live with some form of disability, and the WebAIM Million report consistently finds that 96%+ of the top million websites have detectable WCAG failures. These aren't obscure edge cases — they're broken buttons, missing labels, and images with no alt text.
Here's what I see most often:
- Images with no alt text — screen readers just say 'image' and move on
- Contrast ratios below 4.5:1 — text becomes unreadable for low-vision users
- No keyboard navigation — if you can't tab through your app, keyboard and switch users are locked out
- Focus traps in modals — the user opens a dialog and can't escape it without a mouse
- Missing form labels — the screen reader has no idea what an input field is for
- Auto-playing video with no pause button — disorienting for users with vestibular disorders
The fastest win is adding automated accessibility checks to your CI pipeline. It won't catch everything — automated tools find maybe 30-40% of issues — but it catches the obvious stuff before it ships.
// 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([]);
});
Five minutes of setup. Runs on every PR. Catches missing alt text, broken ARIA roles, insufficient contrast, and dozens of other failures automatically. There's no excuse not to do this.
Cognitive Overload: The Settings Page From Hell
Human working memory holds about four to seven items at once. That's not a suggestion — it's a hard cognitive limit. When your interface dumps 200 options on a single page with no hierarchy, you're not giving users control. You're giving them decision paralysis.
I once counted the settings in a project management tool we used at work. 347 options. One page. No search. No grouping beyond vague categories like 'General' and 'Advanced.' Half the team had never changed a single setting because the page was so overwhelming they just closed it immediately.
The fix is progressive disclosure. Show the five settings people actually change. Put everything else behind clearly labeled expandable sections. Add a search bar. And for the love of everything, provide sensible defaults so most users never need the settings page at all.
If your settings page needs its own documentation, your settings page is the problem.
Notification Overload Trains Users to Ignore Everything
When every event triggers a notification — a teammate joined a channel, someone reacted with an emoji, a deploy finished, a calendar event starts in 30 minutes — users learn to ignore all of them. You've turned your notification system into white noise. The one critical alert that actually matters? Buried under 47 unread notifications.
Conservative defaults are the answer. New users should get critical notifications only. Batch non-urgent stuff into a daily digest. And always — always — let users mute or customize directly from the notification itself, not from some buried preferences page three clicks away.
Slow UI Is Broken UI: Performance as a UX Problem
A button that takes two seconds to respond isn't slow. It's broken. Users don't think 'oh, the server is processing my request.' They think 'did my click register?' and they click again. Now you've got duplicate submissions, confused state, and a user who doesn't trust your app.
Anything over 100ms feels laggy. Anything over a second breaks the user's train of thought. Yet we keep shipping multi-megabyte JavaScript bundles, render-blocking third-party scripts, and layout shifts that make people click the wrong element. The fix isn't complicated — it just requires caring about it.
// 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.');
}
});
}
Optimistic updates, skeleton screens instead of spinners, lazy-loaded non-critical JavaScript, and monitoring Core Web Vitals as real metrics — not afterthoughts. These aren't advanced techniques. They're baseline expectations.
Mobile UX Anti-Patterns That Refuse to Die
Mobile has its own special category of UX sins, mostly because developers keep testing on a brand-new iPhone and calling it a day.
Touch Targets That Require Surgical Precision
Apple's guidelines say 44x44 CSS pixels minimum for touch targets. WCAG 2.2 agrees. Yet I still see 20-pixel close buttons on modals, tiny links jammed together in footers, and icon buttons that are basically impossible to hit on a phone. It's even worse for users with motor impairments.
/* 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;
}
Full-Screen Interstitials That Block Content
You tap a search result. The page loads. Before you can read a single word, a full-screen modal demands your email address. You haven't even seen the content yet. Why would you subscribe?
Google has penalized intrusive interstitials in search rankings since 2017. They still persist because someone, somewhere, is looking at a dashboard that shows a 2% email capture rate and calling it a win — while ignoring the 40% bounce rate it creates. Use inline banners or bottom sheets. Wait until the user has actually engaged with your content before asking for anything. A reader who's finished three articles will subscribe willingly. A reader who's been interrupted three times will leave and never come back.
How to Build a UX Quality Culture (Not Just Fix Individual Bugs)
Fixing anti-patterns one at a time is necessary but not sufficient. If your process keeps producing them, you need a better process. Here's what actually works:
- Add axe-core to your CI pipeline — automated a11y checks on every PR
- Make UX review part of code review, not a separate design-only process
- Test with 5 real users before shipping major features — 5 users surface ~85% of usability problems
- Track task completion rates and error rates, not just page views and time-on-site
- Use a design system with pre-built accessible components so individual devs aren't solving the same interaction problems from scratch
- Dogfood your own product relentlessly — when the developer who built the settings page has to use it daily, it gets fixed fast
The best UX improvement I ever made started with me trying to use our own checkout flow on my phone and rage-texting the product channel at 11pm.
Every anti-pattern in this article has a straightforward fix. None of them require bleeding-edge technology or massive redesigns. They require giving a damn. Start with one fix today. Run one accessibility audit this week. Test with one real user this month. Good UX isn't about grand gestures — it's about consistently choosing respect for your users over short-term metrics. The gap between software people tolerate and software people love is smaller than you think. It's just a thousand small decisions, made well.