Frontend developers spend most of their energy on what happens before the browser sees anything: components, state management, data fetching, bundling, server rendering. The HTML that comes out at the end is often treated as a by-product. If it looks right on screen, the job is done.
A recent post on DEV by Jim Smith argues that this view is out of date. The markup your application produces is read by far more than human eyes. Screen readers, end-to-end test suites, browser extensions, search crawlers, monitoring tools and, increasingly, AI agents that operate browsers on a user's behalf all consume the DOM directly. For all of them, your HTML is an interface. Smith's framing is that HTML is effectively an API surface, and like any API it works better when its meaning is explicit rather than implied.
We think this is one of the more practical ideas in frontend development right now, so here is our take on the patterns he describes, why they matter and how to apply them.
Use real buttons for actions
The classic shortcut is a div with a click handler, styled to look like a button. Visually it can be indistinguishable from the real thing. Structurally it is just a generic box. It does not receive keyboard focus by default, it is not announced as a control by assistive technology, and automation tools have to guess what it is.
A native button element gives you focus handling, keyboard activation, the correct role and a recognisable control for free. In React, Vue or any other framework, this is a one-word change in the component, and CSS can make both versions look identical. The difference is entirely in what the page means.
Make form relationships explicit
Placeholder text is often asked to do too much: act as the label, the instruction and the example all at once, and it disappears as soon as the user starts typing. A visible label element tied to its input with the for and id attributes, an appropriate input type, a name, an autocomplete hint and a required attribute tell the browser and any other software exactly what the field is.
Error messages deserve the same care. Instead of a free-floating "invalid value" line, link the message to the field with aria-describedby and mark the field with aria-invalid. A tool reading the page then knows which input has the problem and why.
Don't keep state only in CSS
A faded button with a disabled class looks unavailable to a human, but the state exists only in the styling. Use the native disabled attribute, or aria-disabled when you need the element to remain focusable. The same idea applies to toggles, which can expose aria-pressed, and to expandable sections, which can use aria-expanded together with aria-controls. CSS should visualise state; the markup should carry it.
Write links that say where they go
A dashboard full of identical "Learn more" links is easy for a sighted user to decode from context, but to software they are indistinguishable. Descriptive link text such as "Learn more about billing" helps screen reader users, who often navigate by a list of links, and it also produces better tests. A test that finds a link by its role and accessible name survives a redesign, while one that relies on the third card's footer anchor does not.
Let dynamic content announce changes
Modern interfaces update asynchronously all the time. When a button triggers a fetch and the result appears a moment later, the change is obvious to the eye but invisible to anything reading the structure. A container with role status and aria-live polite tells assistive technology to announce the update. For loading regions, aria-busy can signal that content is being refreshed and then flip back when it is ready. Observable state transitions make an app easier to use without sight and easier to automate, because tools can wait for a defined state instead of an arbitrary timeout.
Use stable identifiers sparingly and deliberately
Selectors built on utility classes or element positions describe layout, not meaning, and break with the next restyle. A dedicated attribute such as data-testid is more stable, but Smith cautions against adding it everywhere. The better order of preference is a meaningful role and name first, then a stable business identifier where several controls legitimately look alike or where third-party integrations depend on them, and only then implementation-specific selectors. In short, avoid letting CSS class names become part of your application's external contract.
This matches the official Playwright guidance, which recommends locating elements by user-facing attributes such as role, label and accessible name, and treats test IDs as a fallback for cases where those are not enough.
Treat important data as data
A banner saying "Only $49!" leaves a lot unsaid. Is that per month, per year, a setup fee, a starting price? Spelling out the plan name and billing period in the visible text removes ambiguity for everyone. For products and offers, structured data using the schema.org vocabulary can make price, currency and availability machine-readable. Smith adds an important caveat: structured data must match what users actually see. It should clarify the interface, not describe a different version of reality.
The payoff: tests that express intent
A useful side effect of all seven patterns is more resilient automated testing. A Playwright test that clicks the second pricing card's primary button breaks when a designer reorders the cards. A test that clicks the button named "Start Professional plan" keeps working, and reads like a description of what the user is doing. Smith compares it to API design: a URL like users/42/subscriptions tells you far more than thing/3/value/2.
He also recommends a habit few developers have: inspecting the accessibility tree. Browser developer tools can show how the browser interprets each element. If an icon-only control appears there with no name, that is a problem for screen reader users, for test authors and for any agent trying to operate the page.
Frameworks are not the problem
It is tempting to blame component frameworks for poor markup. As the article points out, a Button component renders a div only because someone wrote it that way. React, Vue, Svelte, Angular and server templates can all produce excellent semantic HTML. The abstraction does not remove the need to understand the platform underneath.
Practical takeaways for teams
Audit one critical flow this week, such as sign-up or checkout. Try it with only the keyboard and check the accessibility tree for unnamed or mislabelled controls.
Fix your shared component library first. Correcting a Button, Input or Toggle component once improves every screen that uses it.
Make role-based locators the default in your end-to-end tests, and treat a hard-to-locate element as a signal that the markup needs work.
Add accessibility linting to CI so regressions are caught in review rather than by users.
Keep structured data generated from the same source as the visible content, so the two cannot drift apart.
Semantic HTML used to be pitched mainly as an accessibility obligation. It still is, and that alone is reason enough. But with more software reading and operating web interfaces every year, including AI agents, clear markup is also becoming a matter of reliability and integration. Treating your HTML as a contract is one of the cheapest quality improvements a frontend team can make.
Source: Jim Smith, DEV Community, original article linked below.
Cover photo: Markus Spiske, CC0, via Wikimedia Commons.