The setup: I thought I was "pretty good" at a11y
I shipped a client site earlier this year that I was genuinely proud of. Fast, clean, nice motion, sensible structure. I had aria labels sprinkled in. Focus states visible. Color contrast checked with a browser extension.
I walked around thinking: this is solid. Not perfect, but above average. Better than the usual marketing-page glitter I see launch on Product Hunt.
The client then told me they had booked an external accessibility audit. Corporate policy. Third-party vendor. Full WCAG report.
I said "nice, happy to collaborate". What I meant was "sure, let them rubber-stamp my genius".
Yeah. That did not happen.
The report that wrecked my ego
The PDF landed in my inbox on a Thursday. 43 pages. That already hurt. The executive summary used the phrase "significant barriers" twice, which hurt more.
I did what every developer does with long reports. Scanned for screenshots of my worst sins first. There they were. Highlight annotations. Red circles. Yellow boxes. My UI looked like a football coach diagram.
Here is the uncomfortable bit. Nothing in that report was "clever". No edge-case academic stuff. It was all basic things I should have caught.
I am going to walk through the worst ones and how they changed my process, because theory-level accessibility feels abstract. Getting slapped with real user failures is not.
1. My fancy focus trap was an actual trap
The site had a beautiful modal. Blurred background, springy animation, keyboard trap implemented with a tiny utility I had used before. I was proud of it.
The auditor flagged it as a critical issue.
During keyboard navigation tests the focus would enter the modal, cycle through the fields, but the close button was not reachable with Tab. Why? Because I had added a little micro-interaction that hid the close label visually on small screens and replaced it with an icon-only button.
The button itself was still there, but my focus ring was styled only for button:focus-visible. On that element I had a weird outline offset. Combined with the layout, the visible outline ended up outside the viewport on some sizes.
So technically the element got focus. Practically the user saw nothing. It looked frozen.
What I had never done before, and what the auditor did, was this:
- Turn off the mouse completely.
- Navigate the entire site with Tab and Shift+Tab at 1x speed. No cheating, no skipping.
- Try to escape every overlay, dialog, and menu with only the keyboard.
When I repeated that, slowly, I realised how brittle my focus handling really was. It worked in my happy path short test. It fell apart when you used it like a real person who is stuck inside a component.
Change in my workflow: I now do a "keyboard only" pass on every feature. I literally put my trackpad out of reach and try to get stuck. If I can trap myself, I fix it before launch.
2. Screen reader order did not match the visual order
The homepage hero was a pretty typical pattern. Left column had a headline, paragraph, CTA buttons. Right column had a product mockup with some floating badges.
I had built the layout using CSS grid and some reordering. For mobile I wanted the image first visually, so I used order on flex items in one breakpoint. It looked great.
The auditor ran this through NVDA and VoiceOver. Their note was blunt. The reading order was: image, decorative badge, secondary badge, then suddenly footer navigation, then only after that the main hero copy.
I had wrapped the visual stuff in too many nested containers. The DOM structure was not in the logical content order. I relied on CSS to rearrange what the user saw, but the assistive tech still obeyed the DOM.
This is the kind of bug you do not catch with Lighthouse or simple checklists. You have to actually turn on a screen reader and listen to your page. Like a podcast of your mistakes.
Change in my workflow: I now treat DOM order as the source of truth. If I have to reorder visually, I pause and ask why. Can I instead structure the HTML in the same order as the content should be consumed, and use layout only for spacing and alignment?
Most of the time, yes. My old approach was just lazy. Flexbox gave me a hammer and I reordered everything.
3. Low-contrast on active states, not static ones
I had checked the color palette with a standard 4.5:1 AA contrast checker. Base text, buttons, links. All fine.
The auditor did not just check static UI. They checked states. That part annoyed me, because I knew they were right and I had never actually done it properly.
On hover I darkened some buttons. On focus I lightened outlines. On form fields I added a subtle colored border and a glow when active. Subtle was the problem.
On one primary button, the normal state contrast was 4.7:1. The hover state dropped to 3.2:1 because I loved how that slightly softer shade looked against the background image.
Looks great to me. Useless if you have low vision and rely on the difference between states to know where you are.
The same happened with inline error messages. Neutral grey text that turned red on error. That red had worse contrast against the light background than the original grey did, especially in the small labels.
Change in my workflow: I started checking contrast per state. Normal, hover, focus, disabled. If a designer gives me a Figma file with 9 different button variants, I run the text color vs background for each one.
It is boring. It also exposed that some of the prettiest variants were the most hostile to real users. We adjusted the palette. The world kept turning. Nobody complained that the shade of red was not "brand authentic".
4. Interactive elements pretending to be divs
This one was straight up laziness. I thought I had grown out of custom clickable divs. Apparently not.
We had a row of feature cards. Clickable, nice hover animation, whole card usable as a hit area. I built it with a <div role="button"> wrapped around some content, added a click handler, and made it submit a filter.
The auditor flagged it as a keyboard trap because you could Tab into the card, press Enter, nothing happened. Space triggered click, Enter did not. Also there was no semantic connection between that "button" and the filter results that updated below. No ARIA live region, no announcement of the change.
The simple fix: use a real <button>. Or a <a> link if it navigates. Let the browser give you the semantics, keyboard handling, and basic announcements for free.
I know this. You know this. We all still ship rogue divs under time pressure.
Change in my workflow: I now scan my own code for role="button" during review. If I see it, I treat it as a smell. Ninety percent of the time, it is just a button that should have been a button.
5. Hidden labels behind clever UI
The search input in the header was the part of the design I felt most smug about. Minimal placeholder text, small icon, expands nicely on focus. Very tidy.
The auditor noted that the input had no accessible name. I had hidden the visual label and not actually left a label element tied to the input with for and id.
I had gone for the quick hack: placeholder as label. Screen readers treat that poorly, and placeholder text vanishes once you type. For someone using dictation or a screen reader, that label is how you know what the field is for when you come back to it.
They showed me a screen reader log that literally read out: "Edit text, blank". No indication that it was a product search.
Change in my workflow: now every input has a label. If the design wants it hidden, I use a visually hidden utility class, not an absent label. I also stopped using placeholder text as the only instruction. It is now, at best, secondary.
6. Motion that cannot be turned off
This one hurt because I love motion. Micro interactions are my candy. I spend time tuning easing curves and stagger timings.
The site had a scroll-triggered animation sequence. Elements sliding in, fading, parallax on hero images. It all respected prefers-reduced-motion in theory. Or so I thought.
I had wrapped the main animations in a media query that checks for reduced motion. Cool. Except we later added a third-party library for one section that did not care about the user preference.
The auditor tested with reduced motion set on the OS. The site still threw in a bunch of autoplaying motion as you scrolled through that section. Annoying for some people. Nausea-inducing for others.
Change in my workflow: I now test with reduced motion enabled on my own machine at least once per feature. Not just trust my CSS. If I bring in a library, I check how to disable or tame its animations. If I cannot, I reconsider using it.
7. Error handling that assumes everyone can see red
Form errors were my silent shame. They worked visually. Red text below the field. Little icon. Subtle shake animation on submit.
The auditor tested with color blindness simulation and with a screen reader. Two problems appeared immediately.
- Color was the only indicator for error state. No icon or pattern or text like "Error:".
- Focus did not move to the first invalid field when the form failed.
Screen reader users hit submit, heard "Form submission failed", then nothing helpful. They had to hunt manually for what went wrong.
The fix was mechanical. Wrap errors in aria-live="polite" regions, move focus to the first error, add a clear text label like "Error: Email is required" instead of a vague "This field is required".
Change in my workflow: I now test form failure states with a keyboard and with the dev tools color blindness filters. If I cannot find the error instantly without relying on red, it is not good enough.
What changed permanently in my process
The biggest lesson was not a single bug. It was the realisation that my informal "I know the basics" attitude created invisible cliffs for actual people.
Here is what stuck and what I now do almost by default on client work.
1. One manual pass per feature
Every major feature now gets a dedicated accessibility pass before I call it done. Ten to fifteen minutes. Keyboard only. Reduced motion on. Screen reader on for the key flows.
It is not a full audit. It is enough to catch the worst sins before an external auditor has to embarrass me again.
2. DOM order first, layout second
I stopped using order and clever grid tricks to rearrange content just to hit a visual spec. I talk to designers earlier now.
If the Figma layout forces weird DOM gymnastics, I push back. Not aggressively. Just with a clear explanation: "If we do it like this, screen readers will read the footer before the main content." That line works every time.
3. No more divs pretending to be controls
This became a hard rule for myself. If it is clickable and not a drag handle or some weird canvas thing, it must be a real button or a link.
I am fine with extra wrappers for styling. I am not fine with rebuilding basic semantics from scratch because I am chasing a tiny CSS convenience.
4. States, not just components
Design systems love showing the perfect, static state of a component. Accessibility issues show up in the in-between states. Hover, active, loading, error.
So I started building a little "state story" for important components. Button in all states, form in success and failure, dialog open and closing. Then I run through them with keyboard and screen reader.
The humility piece
Getting that audit back was not fun. It made me feel like a junior again. I had shipped a polished-looking site that still put real barriers in front of real users.
On the other hand, that discomfort did something useful. It pushed accessibility out of the "nice to have" mental category and into "part of being a competent frontend dev" for me.
I do not think you become good at this by reading long guidelines. You become good by having someone else try to use your work while you watch your assumptions crumble.
If you have never had an external audit on something you are proud of, I recommend it. Not because it feels good. Because it will flatten the ego you have built around "I know the basics" and replace it with actual, practical habits.
I know mine did.
Subscribe to my newsletter to get the latest updates and news
Member discussion