Things HTML Can Do Without JavaScript: Stop Building Everything Out of divs

Accordions, popovers, modals, autocomplete, form validation, and lazy-loaded images. A guide to the features you get from plain HTML tags and attributes, no library required. Less code, and accessibility comes for free.

How many lines does an accordion take?

Say you're building a "click to expand" FAQ. It usually starts like this:

  • Build the title and content out of divs
  • Attach a click handler
  • Track an isOpen state
  • Add tabindex and a keydown handler so it opens from the keyboard
  • Keep aria-expanded in sync for screen readers

But HTML already has a way to do this with two tags. The example right below actually works. Try clicking it.

Is this accordion really HTML only?

Yes. Not a single line of JavaScript, just the details and summary tags. It opens from the keyboard (Tab → Enter), and screen readers announce whether it's expanded or collapsed.

Browsers give you far more out of the box than most people expect. Before building something yourself, check whether HTML already has it. You'll write less code, and accessibility and performance come for free.

This post collects those features you get from HTML alone.


1. Accordion: <details> + <summary>

<details>
  <summary>How long does shipping take?</summary>
  <p>1–3 business days.</p>
</details>
  • Add the open attribute to start expanded.
  • details elements that share the same name attribute become an exclusive accordion: opening one closes the others automatically.
<details name="faq"><summary>Question 1</summary>Answer 1</details>
<details name="faq"><summary>Question 2</summary>Answer 2</details>
<details name="faq"><summary>Question 3</summary>Answer 3</details>

The name attribute has been supported in all major browsers (Chrome, Safari, Firefox) since 2024. Browsers without support simply treat it as an accordion where several items can be open, so nothing breaks.


2. Popovers: the popover attribute

Dropdown menus, tooltips, notification panels, anything that "pops up when you press a button" can be done with the popover attribute.

<button popovertarget="menu">Open menu</button>

<div id="menu" popover>
  <a href="/profile">Profile</a>
  <a href="/logout">Log out</a>
</div>

That alone gets the browser to handle a surprising amount for you:

What you used to build yourselfWhat popover does by default
Open/close stateThe popovertarget button toggles it
Close on outside clickBuilt in (light dismiss)
Close on EscBuilt in
z-index battlesRendered in the top layer, so it's always on top
Close the previous popover when another opensBuilt in

If you've ever written z-index: 99999, the second-to-last row should feel especially good.


3. Modals: <dialog>

For windows that block the page behind them, like confirmations or login modals, use dialog. You need one line of JavaScript, just to open it.

<button onclick="confirmDialog.showModal()">Delete</button>

<dialog id="confirmDialog">
  <p>Are you sure you want to delete this?</p>
  <form method="dialog">
    <button value="cancel">Cancel</button>
    <button value="ok">Delete</button>
  </form>
</dialog>
  • Opening it with showModal() blocks clicks on the background and traps focus inside the modal.
  • Pressing a button inside form method="dialog" closes it automatically, and the pressed button's value ends up in dialog.returnValue.
  • Esc closes it by default.
  • Style the background with CSS ::backdrop.
dialog::backdrop {
  background: rgb(0 0 0 / 0.5);
}

Not sure whether to use popover or dialog? Remember: if it must block the page behind it, use dialog (modal); if it just floats on top without blocking, use popover.


4. Autocomplete: <datalist>

Showing suggestions in a search box doesn't need a library either.

<label for="framework">Framework</label>
<input id="framework" list="frameworks" />

<datalist id="frameworks">
  <option value="Nuxt"></option>
  <option value="Next.js"></option>
  <option value="SvelteKit"></option>
  <option value="Astro"></option>
</datalist>

The list filters as you type, and users can still enter values that aren't in it. You can't restyle it much, so it's a better fit for admin pages and internal tools than for a product's main search bar.


5. <input> attributes that take care of mobile keyboards

If you take every input with type="text", mobile users are switching keyboards by hand every time.

AttributeEffectUse it for
type="email"Keyboard with @ + format checkEmail
type="tel"Phone keypadPhone numbers
inputmode="numeric"Number keypad (value stays a string)Verification codes, card numbers
enterkeyhint="search"The keyboard's Enter key becomes "Search"Search boxes
autocomplete="one-time-code"Suggests the code from an SMS above the keyboardPhone verification
type="date" / type="color"Native date / color pickersSimple date and color input
<!-- Field for a 6-digit code received by SMS -->
<input
  inputmode="numeric"
  autocomplete="one-time-code"
  maxlength="6"
  enterkeyhint="done"
/>

Don't use type="number" for card numbers or verification codes. Leading 0s disappear, and the mouse wheel can change the value. For "a string that just needs a number keypad," inputmode="numeric" is the right answer.


6. Form validation: attributes + one line of CSS

A lot of input validation can be done with HTML attributes.

<form>
  <input type="email" required placeholder="Email" />
  <input
    type="password"
    required
    minlength="8"
    pattern="(?=.*\d)(?=.*[a-zA-Z]).*"
    title="Include both letters and numbers"
  />
  <button>Sign up</button>
</form>
  • required, minlength, maxlength, min, max, and pattern cover the basic checks, and on submit the browser blocks the form and tells the user what's wrong.
  • For error styles, use CSS :user-invalid. Unlike :invalid, it only turns red after the user has interacted with the field, so the page doesn't load already covered in red.
input:user-invalid {
  border-color: crimson;
}

HTML validation is for user convenience, not security. Anyone can delete the attributes in dev tools, so always validate again on the server.


7. Images: performance from a few attributes

<img
  src="/images/hero-800.jpg"
  srcset="/images/hero-400.jpg 400w, /images/hero-800.jpg 800w, /images/hero-1600.jpg 1600w"
  sizes="(max-width: 600px) 100vw, 800px"
  width="800"
  height="450"
  loading="lazy"
  decoding="async"
  alt="A developer writing code in front of a laptop"
/>
AttributeWhat it does
loading="lazy"Delays downloading the image until it's near the viewport (lazy loading)
width + heightReserves space before the image loads so the page doesn't jump (reduces CLS)
srcset + sizesDownloads only the resolution that fits the screen
altAlternative text read by screen readers and search engines

Don't put loading="lazy" on large images visible on first load (hero images). They'll show up later and hurt your LCP (Largest Contentful Paint) score. Those images want fetchpriority="high" instead.


8. Small tags that carry meaning

Visually they're no different from a styled span, but they carry meaning, so search engines and assistive tech understand them better.

TagMeaningExample
<kbd>Keyboard inputCtrl + C
<mark>HighlightA matching word in search results
<abbr>Abbreviation (full name on hover)CLS
<time>Machine-readable date<time datetime="2026-10-08">Oct 8</time>
<progress> / <meter>Progress / measurement

The keys, the highlight, CLS, and the two bars in that table are all rendered with the real tags. Hover over CLS. meter changes color on its own once the value crosses its low/high thresholds (the example is at 90%, so it shows the warning color).


9. Semantic tags to use instead of div, at a glance

Semantic tags say "what this region is for" through the tag name itself. Screen reader users rely on them to jump around ("skip to main content"), and search engines use them to pick out the core content more accurately.

If you're writing thisWrite this instead
<div class="header"><header>
<div class="nav"><nav>
<div class="main"><main> (one per page)
<div class="post"><article>
<div class="sidebar"><aside>
<div class="footer"><footer>
<div onclick="..."><button>

The last row matters most. A div with a click handler can't be pressed from the keyboard, and screen readers don't know it's a button. Things you click are buttons; things that take you to another page are as. Following just that rule removes a large share of accessibility problems.


Summary: why this is worth knowing

What you want to buildThe HTML answer
FAQ accordion<details> + <summary> (+ name)
Dropdown / tooltippopover + popovertarget
Confirmation modal<dialog> + showModal()
Search suggestions<datalist>
Number keypad on mobileinputmode="numeric"
Input validationrequired, pattern + :user-invalid
Image performanceloading="lazy", width/height, srcset

Using what the browser gives you gets you three things at once:

  1. Less code. You can delete state management, event handling, and outside-click detection.
  2. Accessibility comes along. The browser already handles keyboard use, focus management, and screen reader announcements.
  3. A lighter bundle. That's one less UI library to install.

Of course, if you need very fine-grained styling or complex behavior, building it yourself or reaching for a library is the right call. But just building the habit of checking "does HTML already have this?" before you decide will make your projects much lighter.