C.W.K.
Stream
Lesson 05 of 05 · published

<div> Is The Last Resort, Not The First

~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:

  1. 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.
  2. Is this a layout/structural wrapper with no meaning? Then <div> is correct.
  3. 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>

External links

Exercise

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.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.