Why “Just Use Native HTML Forms” is Terrible Advice

Four hours. That’s exactly how long I spent fighting a single email input field yesterday afternoon. The accessibility auditor had flagged our checkout form with a painfully generic ticket: “Form inputs lack proper semantic meaning. Just use native HTML attributes.”

I laughed out loud reading it. If only it were that easy.

Well, that’s not entirely accurate — there is this pervasive myth in web development that if you simply stick to the standard HTML specification, everything will magically work. The browser will handle the validation. Screen readers will understand your intent. Users will get a perfect experience. It sounds great in a beginner’s tutorial, but anyone who has built a production-grade web application knows it is a complete lie.

The Betrayal of Basic Attributes

Let’s look at the absolute basics. You throw a required attribute on an input field. You add type="email". You sit back, thinking you did your job.

But your designer looks at it. They hit submit without typing anything, and the browser throws up that ugly, un-styleable default tooltip right in the middle of the screen. Worse, the validation only fires on submit, not when the user tabs away from the field. The UX team hates it. They want inline validation messages that appear below the input the moment it loses focus.

HTML code on screen - Programming language and program code on screen laptop programming ...
HTML code on screen – Programming language and program code on screen laptop programming …

And what do you do? You add novalidate to the parent <form> tag to kill the ugly browser tooltips. Congratulations. You just completely disabled the native HTML validation UI you were supposed to rely on.

Now you have to rebuild the entire concept of a “required field” from scratch using JavaScript and CSS. You are officially off the paved road.

Welcome to ARIA Hell

Once you abandon native validation behavior, you have to manually wire up the accessibility state. This is where things get genuinely miserable.

You need to tell the screen reader that the field is invalid. So you add aria-invalid="true". But the user also needs to know *why* it’s invalid. So you render a little red text element below the input: “Please enter a valid email address.”

How do you connect that text to the input? The specification says you should use aria-errormessage. I tried this exact setup last week while migrating an old codebase to React 19.0.4.

And here is the gotcha that wasted my entire afternoon: I found out the hard way that aria-errormessage does absolutely nothing in Safari with VoiceOver unless the input element has aria-invalid="true" explicitly set at the exact same millisecond the error text is injected into the DOM. Because of how our state management was batching updates, the error text was mounting a fraction of a second after the invalid attribute was flipped. VoiceOver completely ignored it. Complete silence.

HTML code on screen - Pro-Code Development: What Sets It Apart and Why It Matters
HTML code on screen – Pro-Code Development: What Sets It Apart and Why It Matters

I had to rip out the conditional rendering entirely. Instead of conditionally mounting the error message, I had to leave it in the DOM permanently and toggle its visibility with CSS display: none. I tested this on Chrome 142 with NVDA 2025.3 on my Windows machine, and then back on my Mac with VoiceOver. It finally read the error aloud. It felt like I was fixing a bug from 2014, not 2026.

Most developers don’t know this stuff. They just slap aria-describedby on the input and point it to the error ID, which technically works but creates a terrible experience where the screen reader reads the error message every single time the user focuses the input, even if they’ve already fixed the typo.

The ElementInternals Escape Hatch

The core problem is that HTML was designed for documents, not complex interactive state machines. The attributes we have—maxlength, pattern, min, max—are rigid. They don’t map to the reality of modern UI requirements where an input might be valid only if it matches another input, or if an asynchronous database check clears the username.

HTML code on screen - HTML: How to write, learn & use it
HTML code on screen – HTML: How to write, learn & use it

Lately, I’ve probably stopped trying to hack standard inputs entirely. I’ve started leaning heavily into the ElementInternals API.

If you haven’t played with it yet, it lets you build a custom web component that participates in form submission and validation exactly like a native input, but you control the UI completely. You call internals.setValidity() directly from your JavaScript logic. You don’t have to juggle fifty different ARIA attributes trying to trick the browser into doing what you want.

I benchmarked a heavy checkout form using this approach against our old ARIA-heavy implementation. We stripped out over 400 lines of complex state-syncing logic. The DOM node count dropped by 15%, and the screen reader experience was actually predictable because we were hooking into the browser’s underlying form API, not just painting ARIA attributes on top of generic divs.

By mid-2027, I expect the Open UI initiative to finally land some of the customizable form controls they’ve been promising, which might make this easier. Until then, don’t let anyone shame you for not “just using native HTML.” Native HTML forms are broken for modern use cases. We’re all just taping over the cracks.

Your email address will not be published. Required fields are marked *

Zeen Social Icons