Cool native HTML elements you should already be using

← back   //   Published March 6th, 2025
Table of Contents

I’m constantly surprised by the native HTML spec. New features are regularly added, and I often stumble on existing, handy elements. While often not as versatile as their JS counterparts, using them avoids bloating your app with extra Javascript libraries or CSS hacks.

If this article helps just a single developer avoid an unnecessary Javascript dependency, I’ll be happy. Native HTML can handle plenty of features that people typically jump straight to JS for (or otherwise over-complicate).

I cover some great HTML elements in this article — modals, accordions, live range previews, progress bars and more. You might already know some of these, but I bet there’s something new here for you too.

Note: A few examples use a sprinkle of Javascript in their oninput properties. Please forgive me for this 😅. The elements are self-contained enough that I consider them ~HTML.

Native HTML Modals (dialog)

I’ve led with native HTML modals since I think they’re a common pattern in many apps. People tend to reach directly to Javascript for modals, unaware of the powerful native option.

HTML modals — represented by <dialog> — have extensive support for styling, controls etc, and in my opinion, are one of the most slept-on native HTML elements.

By leveraging the Popover API, dialogs can be opened and closed in native HTML; Opened by clicking a button with a popovertarget referencing the <dialog> element, and closed by clicking a similar button with popovertargetaction="hide", or pressing the esc key.

You can also open / close dialogs in Javascript by calling .showModal() & .closeModal().

Click the button below for an example:


I'm a native HTML modal, built using the dialog element.

Click the "X" to close me.

<button popovertarget="modal">Open Modal</button>

<dialog id="modal" popover="auto" class="max-w-2xl backdrop:backdrop-blur-sm">
  <p>Click the "X" to close me.</p>
  <button popovertarget="modal" popovertargetaction="hide">x</button>
</dialog>

The entire modal is native HTML. Pressing the [x] to close the modal uses the Popover API (or you can use the esc key).

A few things to note:

  • The first button has popovertarget="modal", matching the dialog’s ID, and the dialog has popover="auto". With these two parameters set, clicking the button will open the dialog.
  • The second button closes the modal. It has popovertarget="modal" and popovertargetaction="hide", closing the modal when clicked.
  • We style the modal’s background with the backdrop: Tailwind CSS selector. For regular CSS, you can use the .modal::backdrop pseudoclass.

Originally, to open the <dialog>, I used a button with onclick="document.getElementById('modal').showModal()". A very smart reader pointed out though that the popover spec supports opening dialogs with native HTML.

Thanks to Matthew for the tip!

MDN Docs (dialog)   //   Caniuse (96%)

Native HTML Accordions / Disclosures (details and summary)

Accordions are another classic component that you can easily build with plain HTML. By combining the <details> and <summary> tags you can do some pretty cool things.

Here’s a basic example:


Native HTML Accordion (click to expand) You can build accordions/disclosures with plain HTML, just like this one!
<details>
  <summary>Native HTML Accordion (click to expand)</summary>
  You can build accordions/disclosures with plain HTML, just like this one!
</details>

You can also supply the open property to default the <details> element open (it can still be collapsed):


Native HTML Accordion (default open) You can make a details accordion open by default with the open property.
<details open>...</details>

You can also create exclusive accordions by supplying a matching name parameter to multiple <details> elements. This means only one group from the accordion can be open at a time:


Frogs 🐸🐸🐸🐸🐸🐸🐸🐸🐸🐸🐸🐸
Dogs 🐶🐶🐶🐶🐶🐶🐶🐶🐶🐶🐶🐶
Cogs ⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️⚙️
<details name="group">
  <summary>Frogs</summary>
  🐸
</details>

<details name="group">
  <summary>Dogs</summary>
  🐶
</details>

<details name="group">
  <summary>Cogs</summary>
  ⚙️
</details>

MDN Docs (details)   //   Caniuse (97%)

Native HTML Range Inputs

HTML range inputs are nothing new. By default they look like this, , and allow users to input a value on a sliding scale.

You can extend the functionality of the basic range input by adding tickmarks and a live value output.

Range inputs with ticks (datalist)

By supplying a <datalist> to a range input, you can render tickmarks at each value step and clamp the input value to one of the datalist options.


2
<input type="range" min="1" max="5" list="steps" />

<datalist id="steps">
  <option>1</option>
  <option>2</option>
  <option>3</option>
</datalist>

Range inputs with live value display

By adding a tiny bit of Javascript to the range input, you can easily add a live display of the input value. Credit goes to this excellent StackOverflow answer for figuring this out!


50
<input
  type="range"
  value="50"
  min="1"
  max="100"
  oninput="this.nextElementSibling.value = this.value"
/>
<output>50</output>

MDN Docs (range)   //   Caniuse (98%)

Native HTML Progress Bars (progress)

These days, browsers support a basic, fully native HTML progress bar:


<progress value="6" max="10"></progress>

With some styling, you can make a progress bar look like the one below. I’ve used TailwindCSS for the styling, but you can apply them directly with CSS for the same result:


<progress
  class="[&::-webkit-progress-bar]:rounded-lg [&::-webkit-progress-value]:rounded-lg   [&::-webkit-progress-bar]:bg-stone-300 [&::-webkit-progress-value]:bg-stone-400 [&::-moz-progress-bar]:bg-stone-400"
  value="6"
  max="10"
></progress>

You can learn more about styling the <progress> element with Tailwind CSS from this GitHub thread.

MDN Docs (progress)   //   Caniuse (97%)

Searchable dropdowns and grouped lists (datalist and optgroup)

You can enhance the basic HTML list input to make it searchable or grouped with the <datalist> and <optgroup> elements.

Searchable dropdowns with datalist


<label for="browser">Type something to search the list:</label>

<input list="browser-names" name="browser" id="browser" />
<datalist id="browser-names">
  <option value="Edge" />
  <option value="Firefox" />
  <option value="Chrome" />
  <option value="Opera" />
  <option value="Safari" />
</datalist>

MDN Docs (datalist)   //   Caniuse (97%)

Grouped lists with optgroup


<label for="food">Choose a food:</label>

<select class="border rounded-md" name="food" id="food">
  <optgroup label="Fruits">
    <option value="apple">🍏 Apple</option>
  </optgroup>
  <optgroup label="Vegetables">
    <option value="eggplant">🍆 Eggplant</option>
  </optgroup>
  <optgroup label="Desserts">
    <option value="doughnut">🍩 Doughnut</option>
  </optgroup>
</select>

MDN Docs (optgroup)   //   Caniuse (96%)

Key combinations (kbd)

The <kbd> element is an HTML-native representation of keycombos. It typically appears similar to a <code> element, but it’s more semantic and, being a distinct element, you can style it globally.


⌘ + P

<kbd>⌘ + P</kbd>

Globally targeting the <kbd> tag can simplify your styling — you don’t have to add an extra class or component. Just style <kbd> directly. It’s also more semantically correct to use <kbd> for keyboard combinations, versus littering your pages with <span> elements.

I’ve styled the <kbd> tag to look like this: ⌘ + P

MDN Docs (kbd)   //   Caniuse (96%)

Images with captions (figcaption)

Browsers natively support captioned images with <figure> and <figcaption>. You might find it easier to use these elements instead of implementing the same thing yourself.

Here’s an example:


2Sat Electronics Sculpture
Fig.1 - Electronics Sculpture
<figure>
  <img src="image.jpg" alt="..." />
  <figcaption>My picture</figcaption>
</figure>

MDN Docs (figcaption)   //   Caniuse (96%)

Conclusion

I hope you found this article useful!

I’m constantly surprised by what I can build with plain, native HTML, and it’s always satisfying ripping out a JS package I’ve replaced with a simple HTML tag.