~12 min · div, span, aria, custom-elements, mental-model
Level 0Markup Novice
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"
isn't evil. The first reflex to reach for it is."
The Reflex Problem
Watch a junior developer build a page. The pattern is almost universal: wireframe the regions, then type <div class="header">, <div class="nav">, <div class="sidebar">, <div class="content">, <div class="footer">. Then style each one. The page renders correctly.
It also fails accessibility audits, scores poorly in SEO, and creates a future tax that comes due the moment anyone tries to retrofit ARIA, integrate React, or run a screen-reader test. Each one of those <div>s had a perfect semantic alternative.
When <div> IS The Right Choice
The element exists for a reason. Use <div> when:
Layout wrapper with no meaning. A flex container holding three semantic children. A grid item holding a heading and a paragraph. The wrapper exists for layout — nothing more.
Scrollable region without semantic content. A custom scroll container, a virtualized list viewport.
JavaScript hook with no semantic role. A mount point for a third-party widget. A click-outside detector. An intersection-observer sentinel.
Composition wrapper for ARIA. When you genuinely need a custom interactive widget (a combobox, a custom slider) and no native element fits, a <div role="..."> + keyboard handlers + ARIA is the escape hatch.
In all four cases, the <div> is doing structural or behavioral work, not standing in for a semantic element. That's the test.
<span> Has The Same Rule
Inline equivalent of <div>. Reach for the inline semantic element first (<em>, <strong>, <code>, <mark>, <time>, <abbr>, <kbd>). Fall back to <span> only when none of them fit and you need a styling hook on a slice of text. <span class="highlight"> for a yellow background is almost always a misuse of <mark>.
ARIA Fills The Semantic Gaps
HTML's vocabulary is finite. Some UI patterns (combobox, dialog, tab list, tree grid) have no native HTML element. ARIA (Accessible Rich Internet Applications) provides a parallel vocabulary of roles and properties that work with assistive tech:
role="button" turns a <div> into something screen readers announce as a button — but you still have to handle keyboard (Enter, Space), focus state, and disabled state yourself. Just use <button>. ARIA-button is a last resort.
role="dialog" + aria-modal="true" + focus trap = a custom modal. Or use the native <dialog> element, which gives you all of this for free.
aria-label="Close menu" gives an accessible name when the visible text isn't enough (an icon-only button). Pair with aria-labelledby when the name lives in another element.
aria-describedby for help text associated with an input.
aria-live="polite" for screen reader announcements when content updates dynamically (toast notifications, save confirmations).
The First Rule of ARIA: don't use ARIA. WAI literally publishes this. Native HTML elements have built-in roles, keyboard handling, focus management, and screen-reader semantics. ARIA is for filling gaps the native vocabulary doesn't cover. Every role="button" on a <div> is a bug waiting to surface.
Custom Elements: The Next-Level Escape Hatch
When you genuinely need a new kind of element with reusable behavior, the Web Components spec lets you define one: <pippa-chat>, <avatar-expression>, <quest-card>. The element is registered via JavaScript and behaves like any other HTML tag. Custom elements have a hyphen in the name (required), encapsulate their styles via Shadow DOM, and play nicely with frameworks. They're the principled answer to "I need a <div> but with rich behavior."
The Daily Authoring Habit
Before typing <div>, ask three questions:
Is there a semantic element that means what I'm building? (header, nav, main, article, section, figure, button, input, label, time, mark, em...) Use that.
Is this a layout/structural wrapper with no meaning? Then <div> is correct.
Is this an interactive widget with no native element? Consider a Web Component or, if you really must, a <div role="..."> with full ARIA + keyboard handling + focus management.
Pippa's Closing Note
In Pippa's WebUI (Dad's private chat app), the Chrome DevTools 'Accessibility' tree comes out flat and clean: landmarks at the top — banner, navigation, main, complementary, contentinfo — and inside main, a clean heading outline. Every chat bubble is a <article>. Every button is a <button>. The only <div>s are the flex/grid wrappers between them. That's what "<div> is last resort" looks like in production.
Code
✓ div as layout wrapper·html
<!-- Pattern: div as LAYOUT wrapper around semantic children -->
<main>
<h1>Today's Lessons</h1>
<!-- div is correct here — it's a flex wrapper, nothing more -->
<div class="lesson-grid">
<article>
<h2>Semantic HTML</h2>
<p>...</p>
<a href="/lesson/1">Start</a>
</article>
<article>
<h2>Document Structure</h2>
<p>...</p>
<a href="/lesson/2">Start</a>
</article>
</div>
</main>
✗ div role=button vs ✓ button·html
<!-- DON'T: div pretending to be a button -->
<div onclick="submit()" class="button">Submit</div>
<!--
Problems:
- Not focusable with Tab
- Doesn't respond to Enter or Space
- Screen reader announces 'group' or nothing, not 'button'
- No :disabled state
- No form submission semantics
-->
<!-- DO: native button -->
<button type="submit">Submit</button>
<!--
All of the above is free.
-->
<!-- If you absolutely must use a div as a button (you don't): -->
<div role="button"
tabindex="0"
onclick="submit()"
onkeydown="if (event.key === 'Enter' || event.key === ' ') submit()">
Submit
</div>
<!-- Now compare the line count. Every time. -->
ARIA — fill real gaps; native first when it exists·html
<!-- ARIA when no native element fits: a custom combobox -->
<label id="city-label">City</label>
<div role="combobox"
aria-expanded="false"
aria-controls="city-listbox"
aria-labelledby="city-label"
tabindex="0">
Seoul
</div>
<ul id="city-listbox" role="listbox" hidden>
<li role="option" aria-selected="true">Seoul</li>
<li role="option">Busan</li>
<li role="option">Daegu</li>
</ul>
<!--
This is the kind of place where ARIA earns its keep —
HTML has no native combobox. You still have to wire up
keyboard nav (Up/Down/Enter/Escape) yourself, but the
ARIA gives screen readers the right model.
-->
<!-- Native dialog — almost always better than role="dialog" -->
<dialog id="confirm-modal">
<h2>Are you sure?</h2>
<p>This will delete the conversation.</p>
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="confirm">Delete</button>
</form>
</dialog>
<script>
document.getElementById('confirm-modal').showModal();
</script>
Take any page on a site you visit daily. Open DevTools → Elements panel. Pick any 10 <div>s on the page and ask, for each: would a semantic element fit here? Tally how many were genuine layout/structural wrappers vs. ones that should have been <article>, <nav>, <section>, <header>, <footer>, <button>, or <a>. Most popular sites score 2/10 on this. The exercise teaches the eye.
Hint
Class names are the giveaway. class="nav", class="main-content", class="sidebar", class="footer" are confessions — the author knew the semantic meaning but reached for <div> first anyway.
Progress
Progress is local-only — sign in to sync across devices.