我不再用 div 取代的語義化 HTML 元素
Semantic HTML Elements I Stopped Replacing With Divs
作者分享以五種語義化 HTML 元素取代 div 的實作經驗,涵蓋 button、dialog、details/summary、nav 和 time,以及它們提供的原生鍵盤操作、焦點管理與語義資訊。使用原生標籤可減少自行補寫鍵盤處理與狀態管理;文中亦提醒,表單內的 button 預設會提交,而 details 不會自動令多個項目互斥。
A real button gives focus, Enter and Space keys, and a role for free
dialog handles focus trapping and Escape without any JavaScript
details/summary is a disclosure widget with keyboard support built in
A div with onClick fakes all of this and usually gets it wrong
I used to reach for <div onClick> out of habit. Then I started reading my own markup through a screen reader and found half my interface was invisible to it. Here are the five elements I stopped replacing with divs, and exactly what each one hands you the moment you type the tag.
The button that a div can never fully fake
A <div onClick> looks fine. It renders, it fires the handler, the demo works. Then someone tabs to it and nothing happens, because a div isn't in the tab order. So you add tabindex="0". Now it focuses but Enter doesn't fire the click, because divs don't translate Enter into activation. So you add a keydown listener for Enter. And Space, because native buttons also fire on Space. And you have to call preventDefault on Space so the page doesn't scroll.
Then a screen reader hits it and announces "group" or nothing, because a div has no role. So you add role="button". Now it announces correctly but it doesn't go disabled, so you add aria-disabled and remember to skip the handler when it's set.
Count the pieces you just rebuilt. Tab order, Enter, Space, the scroll prevention, the role, the disabled state.
A real <button> gives you all of that in eleven characters. It's focusable by default, it activates on Enter and Space, it reports role="button" to assistive tech, and disabled actually blocks the click and removes it from the tab order.
The one thing people get wrong: a button inside a form submits that form unless you set type="button". That trips me up maybe once a month. Set the type explicitly on any button that isn't a submit and the surprise goes away.
There's also the styling excuse, which I used for years. "Buttons are hard to style." They're not anymore. appearance: none, kill the border and background, and a button styles exactly like a div. You keep every behavior and lose nothing visually.
My rule now: if clicking it does something, it's a button or a link. A link if it navigates to a URL, a button for everything else.
I don't own a single <div onClick> in production code. Every one I removed deleted three or four lines of keyboard patching with it.
dialog handles the focus trap I always got wrong
Modals are where hand-rolled components quietly fail. A proper modal has to trap focus so Tab doesn't wander behind the overlay, close on Escape, return focus to the element that opened it, and mark the background as inert so a screen reader doesn't read it.
I've written that focus trap by hand. It's a loop that listens for Tab, finds the first and last focusable elements, and wraps the cursor around. It breaks the moment someone adds a new focusable element inside and forgets to update the query.
I've shipped that bug.
The native <dialog> element does the hard parts. Call showModal() and the browser traps focus inside the dialog, dims the rest of the page with a ::backdrop pseudo-element, makes the background inert, and closes on Escape automatically. Focus returns to the trigger when it closes. You get the behavior that took me an afternoon to get almost right, built into the tag.
What you still owe it: move focus to a sensible element when it opens (usually the first input or the close button), and give it an accessible name with aria-labelledby pointing at your heading. That's it. Two lines versus a whole custom hook.
One gotcha worth knowing. show() and showModal() are different. show() opens a non-modal dialog with no focus trap and no backdrop. If you want the real modal behavior, you want showModal(). I mixed them up once and spent ten minutes confused about why Escape wasn't closing anything.
This pairs well with careful state handling. If you're updating data from inside a dialog, I covered the pattern I use in Optimistic UI Updates That Never Lie to the User, because a dialog that closes before the save confirms is its own small lie to the user.
details and summary: a disclosure widget with zero JavaScript
Every FAQ accordion, every "show more" toggle, every collapsible settings panel. I built those with a state variable and a click handler for years. The native version is <details> with a <summary> inside, and it needs no JavaScript at all.
The <summary> is the always-visible label. Everything after it is hidden until you open it. Clicking the summary toggles it. Tabbing to it works, Enter and Space toggle it, and a screen reader announces it as a disclosure widget with an expanded or collapsed state. The browser tracks that state with the open attribute, which you can read or set.
Here's where it quietly beats my old code. When the browser's find-in-page search matches text inside a closed <details>, modern browsers open it so the match is visible.
My handmade accordion never did that. Users searching the page just missed content and never knew.
You can style the disclosure triangle with summary::marker or remove it with list-style: none and draw your own. You can animate the open state in browsers that support it. And you get a toggle event you can listen for if you need to lazy-load content on first open.
The honest limit: <details> is for show and hide. It's not an accordion where opening one closes the others. You can wire that up with a little script listening for toggle, but if you need complex interlocked behavior, know that going in.
For the 90 percent case of "click to reveal," it replaces a whole component with two tags.
I treat this the same way I treat a clean commit history. Less to maintain is less to break. If you want the reasoning behind keeping things small and legible, Writing Commit Messages That Double as Documentation goes through how I think about future-me reading my own work.
nav and time: structure the machines can read
These two don't change how the page looks. They change what software understands about it.
<nav> marks a block of navigation links as a landmark. Screen reader users jump between landmarks to skip straight to the menu or past it. A <div class="nav"> is just a box with no meaning attached. Wrap your main links in <nav> and give it an aria-label if you have more than one (like "Primary" and "Footer"), and assistive tech can list them and let the user pick.
I don't wrap every group of links in <nav>. It's for major navigation blocks, not for three links at the bottom of a card. Overusing it makes the landmark list noisy and defeats the point. One or two per page is usually right.
<time> is smaller but I like it. It wraps a date or time and carries a machine-readable value in the datetime attribute. So you write <time datetime="2026-02-14">February 14</time>. Humans see the friendly text, parsers see the ISO value. Search engines, browser extensions, and calendar tools can read it reliably instead of guessing at your formatting.
The payoff is modest but real. When I publish an article, the <time> element gives the published date a format machines agree on. No ambiguity between day-month and month-day order, which is a genuine problem when your readers span continents.
Neither tag asks much. No JavaScript, no state, no event handling.
You swap a div for the semantic tag and the rendered page is identical. The difference lives entirely in the accessibility tree and in how automated tools parse your content.
That's the theme across all of these: the visible result is the same, and the invisible layer gets dramatically better for free.
If you build with an AI assistant, this is exactly the kind of convention worth putting in your project rules so it stops reaching for divs by default. My setup for that lives in the Claude Blueprint.
Bottom Line
Start with one swap. Open your project, search for onClick on a div, and replace the first one you find with a real <button type="button">. Delete the tabindex, delete the keydown listener, delete the role. The behavior gets better and your file gets shorter.
Do that a few times and the habit flips on its own.
The pattern holds for all five. The native element gives you focus, keyboard handling, and the correct screen reader role the moment you type the tag. The div version makes you rebuild each of those by hand, and you'll miss one. I always did.
None of this is new or clever. These elements have shipped in browsers for years. The only thing that changed for me was deciding to use them instead of reaching for the div I already knew. Pick one page you own, read it with a screen reader for ten minutes, and you'll find the divs that should have been buttons. Fix those first.
來源:DEV Community · dev.to