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 yourself | What popover does by default |
|---|---|
| Open/close state | The popovertarget button toggles it |
| Close on outside click | Built in (light dismiss) |
| Close on Esc | Built in |
| z-index battles | Rendered in the top layer, so it's always on top |
| Close the previous popover when another opens | Built 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.
| Attribute | Effect | Use it for |
|---|---|---|
| type="email" | Keyboard with @ + format check | |
| type="tel" | Phone keypad | Phone 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 keyboard | Phone verification |
| type="date" / type="color" | Native date / color pickers | Simple 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"
/>
| Attribute | What it does |
|---|---|
| loading="lazy" | Delays downloading the image until it's near the viewport (lazy loading) |
| width + height | Reserves space before the image loads so the page doesn't jump (reduces CLS) |
| srcset + sizes | Downloads only the resolution that fits the screen |
| alt | Alternative 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.
| Tag | Meaning | Example |
|---|---|---|
| <kbd> | Keyboard input | Ctrl + C |
| <mark> | Highlight | A 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 this | Write 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 build | The HTML answer |
|---|---|
| FAQ accordion | <details> + <summary> (+ name) |
| Dropdown / tooltip | popover + popovertarget |
| Confirmation modal | <dialog> + showModal() |
| Search suggestions | <datalist> |
| Number keypad on mobile | inputmode="numeric" |
| Input validation | required, pattern + :user-invalid |
| Image performance | loading="lazy", width/height, srcset |
Using what the browser gives you gets you three things at once:
- Less code. You can delete state management, event handling, and outside-click detection.
- Accessibility comes along. The browser already handles keyboard use, focus management, and screen reader announcements.
- 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.