Why Audio Autoplay is Bad for Website UX: A Lesson from HeroUI v3
I closed the browser tab within 3 seconds. The music started blaring from my speakers while I sat in a quiet coffee shop, and everyone turned to look at me. That was my first experience with the HeroUI v3 documentation page.
Turns out, I wasn’t alone.
The HeroUI v3 Backlash: A Real-World Case Study
When HeroUI launched version 3, they made a classic UX mistake that sparked immediate backlash on Reddit. The documentation page had background music that autoplayed — and on mobile, there was no way to mute or pause it.
Here’s what users had to say:
“Looks really good. How do I stop the music on that page though?” — strblr
“On mobile at least, it can’t be muted or paused or anything. Made me immediately close the page without looking at the rest.” — cookies_are_awesome
“It’s a classic UX mistake known since the early 2000s.” — strblr
“Absolutely annoying to listen to that showcase music when all I’m to do is look at some syntax examples.” — AkiStudios1
The pattern is clear: users closed the page immediately. These weren’t just annoyed users — they were potential customers who never got to see the actual product because of an anti-pattern that should have died 20 years ago.
Why Autoplay Audio Fails: The Four Pillars of Failure
After experiencing this firsthand and seeing the reactions, I dug into why autoplay audio is so universally hated. It comes down to four fundamental problems:
1. Loss of Control
Users expect to control their browsing experience. When a website plays audio without consent, it strips away that control. This violates one of Nielsen’s heuristics: User Control and Freedom.
Think about it from the user’s perspective:
- They didn’t ask for audio
- They can’t predict when it will start
- They scramble to find controls (if they exist)
- They feel violated, not welcomed
2. Context Mismatch
I was in a coffee shop when the HeroUI music started. Others might be:
- In an office with coworkers nearby
- In a library
- On public transportation
- In a meeting with their microphone on
- Listening to their own music or podcast
The website has zero context about the user’s environment. Autoplaying audio assumes a context that rarely exists.
3. Accessibility Violations
This isn’t just annoying — it’s an accessibility failure. WCAG 2.1 Guideline 1.4.2 (Audio Control) states:
“If any audio on a Web page plays automatically for more than 3 seconds, either a mechanism is available to pause or stop the audio, or a mechanism is available to control audio volume independently from the overall system volume level.”
Autoplay audio creates barriers for:
- Screen reader users: Audio conflicts with screen reader output
- Neurodivergent users: Unexpected stimuli can cause cognitive overload
- Hard of hearing users: Sudden audio can be startling or confusing
4. Trust and Brand Damage
First impressions matter. When a user’s first interaction with your site is “find the mute button,” you’ve already lost. It signals:
- “We care more about our ‘creative vision’ than your experience”
- “We don’t understand basic web standards”
- “We don’t test for real-world scenarios”
The Reddit thread about HeroUI v3? That’s negative word-of-mouth marketing that sticks.
Browsers Agree: Autoplay is Bad
This isn’t just a UX opinion — browser vendors have actively worked to prevent autoplay audio:
┌─────────────────────────────────────────────────────────────┐│ Browser Autoplay Policies │├─────────────────────────────────────────────────────────────┤│ ││ Chrome (since v66, 2018) ││ ┌─────────────────────────────────────────────────────┐ ││ │ ✓ Muted autoplay: ALLOWED │ ││ │ ✗ Autoplay with sound: BLOCKED │ ││ │ • Requires user gesture (click/tap) │ ││ │ • Media Engagement Index (MEI) affects permissions │ ││ └─────────────────────────────────────────────────────┘ ││ ││ Safari / Firefox ││ ┌─────────────────────────────────────────────────────┐ ││ │ ✓ Muted autoplay: ALLOWED (with restrictions) │ ││ │ ✗ Autoplay with sound: BLOCKED │ ││ │ • Requires user interaction │ ││ │ • iOS Low Power Mode blocks all autoplay │ ││ └─────────────────────────────────────────────────────┘ ││ ││ Key Insight: Browsers are UX guardians. ││ If they block your feature, it's probably bad UX. ││ │└─────────────────────────────────────────────────────────────┘Fighting browser policy is a losing strategy. If Chrome, Safari, and Firefox all agree that autoplay audio is problematic, that should tell you something.
The Business Case: Lost Conversions
Let’s talk numbers. When users close your page immediately:
| Metric | Impact |
|---|---|
| Bounce Rate | Skyrockets — users leave before engaging |
| Dwell Time | Plummets — no time to read content |
| Conversion Rate | Drops — bounced users don’t convert |
| SEO Ranking | Suffers — search engines detect poor UX |
| Brand Perception | Damages — negative first impressions persist |
The HeroUI case study shows this in action: multiple potential users closed the page before looking at the documentation. They never got to evaluate the product because of a preventable UX failure.
How to Do Audio Right: Consent-Based Implementation
Let me show you the wrong way (what HeroUI did) and the right way to handle audio.
The Wrong Way (Don’t Do This)
<!-- This is what caused the HeroUI backlash --><audio autoplay loop> <source src="background-music.mp3" type="audio/mpeg"></audio>This code:
- Plays immediately without consent
- Loops forever
- Provides NO controls
- Works… until browsers block it
The Right Way: User Consent First
<div class="audio-player" role="region" aria-label="Background music player"> <button id="playPauseBtn" aria-label="Play background music" aria-pressed="false" > <svg class="play-icon" aria-hidden="true"> <!-- play icon SVG --> </svg> <span>Play Music</span> </button></div>
<audio id="bgMusic" preload="none"> <source src="background-music.mp3" type="audio/mpeg"></audio>const playPauseBtn = document.getElementById('playPauseBtn');const bgMusic = document.getElementById('bgMusic');let isPlaying = false;
playPauseBtn.addEventListener('click', async () => { try { if (isPlaying) { bgMusic.pause(); playPauseBtn.setAttribute('aria-label', 'Pause background music'); playPauseBtn.setAttribute('aria-pressed', 'false'); } else { await bgMusic.play(); playPauseBtn.setAttribute('aria-label', 'Play background music'); playPauseBtn.setAttribute('aria-pressed', 'true'); } isPlaying = !isPlaying; } catch (error) { // Autoplay was prevented - handle gracefully console.log('Playback blocked by browser:', error); isPlaying = false; }});
// Respect page visibility - pause when user leavesdocument.addEventListener('visibilitychange', () => { if (document.hidden && isPlaying) { bgMusic.pause(); isPlaying = false; playPauseBtn.setAttribute('aria-pressed', 'false'); }});WCAG-Compliant Implementation
For full accessibility compliance, you need more than just play/pause:
<div role="region" aria-label="Audio content player" class="audio-container"> <audio id="audioPlayer" preload="metadata"> <source src="podcast-episode.mp3" type="audio/mpeg"> <track kind="captions" src="captions.vtt" label="Captions"> </audio>
<div class="audio-controls"> <!-- Play/Pause --> <button id="playBtn" aria-label="Play" aria-pressed="false"> Play </button>
<!-- Progress bar --> <div class="progress-container"> <label for="progressBar" class="visually-hidden"> Playback progress </label> <input type="range" id="progressBar" min="0" max="100" value="0" aria-label="Seek audio" > <span id="currentTime">0:00</span> <span id="duration">0:00</span> </div>
<!-- Volume control --> <div class="volume-control"> <label for="volumeSlider" class="visually-hidden">Volume</label> <input type="range" id="volumeSlider" min="0" max="1" step="0.1" value="0.8" aria-label="Volume control" > <button id="muteBtn" aria-label="Mute" aria-pressed="false"> Mute </button> </div> </div>
<!-- Text alternative for accessibility --> <details class="audio-transcript"> <summary>Read transcript</summary> <div class="transcript-content"> Full transcript of audio content... </div> </details></div>Key accessibility features:
- Visible controls: All functions have UI elements
- ARIA labels: Screen readers understand each control
- Keyboard navigation: All controls are keyboard-accessible
- Transcript: Deaf/hard-of-hearing users get text alternative
- Visual indicators: Progress bar shows playback position
React/Next.js Component
Here’s a production-ready React component:
import { useState, useRef, useEffect, useCallback } from 'react';
interface AudioPlayerProps { src: string; title: string; transcript?: string;}
export function AudioPlayer({ src, title, transcript }: AudioPlayerProps) { const audioRef = useRef<HTMLAudioElement>(null); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [volume, setVolume] = useState(0.8);
const togglePlay = useCallback(async () => { if (!audioRef.current) return;
try { if (isPlaying) { audioRef.current.pause(); } else { await audioRef.current.play(); } setIsPlaying(!isPlaying); } catch (error) { // Browser blocked autoplay - show user-friendly message console.error('Playback blocked:', error); setIsPlaying(false); } }, [isPlaying]);
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => { if (!audioRef.current) return; const time = Number(e.target.value); audioRef.current.currentTime = time; setCurrentTime(time); };
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => { const newVolume = Number(e.target.value); if (audioRef.current) { audioRef.current.volume = newVolume; } setVolume(newVolume); };
// Update time display useEffect(() => { const audio = audioRef.current; if (!audio) return;
const handleTimeUpdate = () => setCurrentTime(audio.currentTime); const handleLoadedMetadata = () => setDuration(audio.duration); const handleEnded = () => setIsPlaying(false);
audio.addEventListener('timeupdate', handleTimeUpdate); audio.addEventListener('loadedmetadata', handleLoadedMetadata); audio.addEventListener('ended', handleEnded);
return () => { audio.removeEventListener('timeupdate', handleTimeUpdate); audio.removeEventListener('loadedmetadata', handleLoadedMetadata); audio.removeEventListener('ended', handleEnded); }; }, []);
// Pause when component unmounts (page navigation) useEffect(() => { return () => { audioRef.current?.pause(); }; }, []);
const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; };
return ( <div className="audio-player" role="region" aria-label={title}> <audio ref={audioRef} src={src} preload="metadata" />
<div className="controls"> <button onClick={togglePlay} aria-pressed={isPlaying} aria-label={isPlaying ? 'Pause' : 'Play'} > {isPlaying ? 'Pause' : 'Play'} </button>
<input type="range" min="0" max={duration || 0} value={currentTime} onChange={handleSeek} aria-label="Seek" />
<span aria-live="off"> {formatTime(currentTime)} / {formatTime(duration)} </span>
<input type="range" min="0" max="1" step="0.1" value={volume} onChange={handleVolumeChange} aria-label="Volume" /> </div>
{transcript && ( <details> <summary>Read transcript</summary> <div>{transcript}</div> </details> )} </div> );}When Is Audio Actually Appropriate?
I’m not saying “never use audio.” There are legitimate use cases:
| Use Case | Why It’s OK |
|---|---|
| Media players | Users expect and initiate playback |
| Games | Audio is part of the experience |
| Screen readers | Accessibility feature, not content |
| Music/podcast sites | Core functionality, expected behavior |
| Portfolio with video | If controls are prominent and accessible |
Even then, follow these rules:
- Never autoplay with sound — Start muted at most
- Provide obvious controls — Large buttons, clear labels
- One-click dismissal — Users should be able to mute instantly
- Remember preferences — Use localStorage to save mute state
- Mobile-first — Controls must work on touch screens
- Visual indicator — Show when audio is playing
What HeroUI Should Have Done
For a documentation site, background music serves no purpose. But if they really wanted it, here’s the approach:
┌────────────────────────────────────────────────────────────────┐│ HEROUI v3 HOME PAGE ││ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ Hero Section │ ││ │ │ ││ │ "Beautiful UI Components" │ ││ │ │ ││ │ ┌──────────────────────────────────────────────────┐ │ ││ │ │ 🎵 Experience with Sound [▶ Play] │ │ ││ │ │ │ │ ││ │ │ "Click to enable ambient music for the full │ │ ││ │ │ showcase experience" │ │ ││ │ └──────────────────────────────────────────────────┘ │ ││ │ │ ││ └──────────────────────────────────────────────────────────┘ ││ ││ ✓ Audio is OPT-IN, not forced ││ ✓ Clear affordance for interaction ││ ✓ Users who don't care ignore it ││ ✓ Users who want it click once ││ │└────────────────────────────────────────────────────────────────┘The key difference: consent. Users choose to engage with audio, rather than being assaulted by it.
Audit Your Site Today
If you’re reading this and wondering if your own site has autoplay issues, check:
- Search your codebase for
autoplayattributes - Test in multiple browsers — Chrome, Firefox, Safari, mobile
- Test in different contexts — quiet room, headphones, no audio
- Check accessibility — Use a screen reader, try keyboard-only navigation
- Monitor analytics — Look for high bounce rates on pages with audio
The fix is straightforward: remove autoplay, add visible controls, respect user consent.
Related Knowledge
- WCAG 2.1 Guideline 1.4.2: The formal accessibility requirement for audio control
- Chrome Autoplay Policy: Technical details on browser restrictions
- Media Engagement Index (MEI): How Chrome decides which sites can autoplay
- Nielsen’s Heuristics: User control and freedom as a design principle
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments