As frameworks have become more and more commonplace in the world of web development, so too has the over-engineering of features made possible by our humble old friend, HTML.

The mental models that come with using state management in React, Vue and others mean that it’s easy to start off looking to create an accordion for an FAQ section or a progress bar, and before you know it you’ve unnecessarily imported npm packages and half a UI library, along with state functions you probably don’t need.

Modern HTML already has native elements that do a lot of this for you, and they’re often more accessible, lightweight and more straightforward for the developer, too.

Let’s look at five HTML features you probably don’t use enough, and might be over-engineering.

1. Accordions

Accordions are very straightforward with native elements, using <details> and <summary>. The <details> element creates a native open/close widget (accordion), and <summary> defines a clickable heading.

In React, for example, you’d normally use a useState toggle to show or hide content, creating additional code away from the actual HTML, rather than keeping it self-contained with the same functionality using HTML only:

<details>
  <summary>More Info</summary>
  <p>Here’s the hidden content!</p>
</details>

The best part is, this works perfectly inside of JSX, and if you need to control it programmatically, React gives you access to the .open property via a ref.

2. Modals

Modals can be a trap when it comes to frameworks, or at least with React. You start with a simple modal, likely using useState, then begin improving the UX with focus-trapping, ESC key closure, backdrops to control the focus, and it turns into a far bigger job, with edge cases everywhere.

The <dialog> element handles all that natively.

<dialog id="myDialog">
  <p>Hello World!</p>
  <button onclick="this.closest('dialog').close()">Close</button>
</dialog>

<button onclick="myDialog.showModal()">Open Modal</button>

In React, you’d need to add a useRef, and call .showModal() or .close().

const dialogRef = useRef(null);

<button onClick={() => dialogRef.current?.showModal()}>Open</button>
<dialog ref={dialogRef}>
  <p>Hello World!</p>
  <form method="dialog">
    <button>Close</button>
  </form>
</dialog>

It’s likely very similar to your current approach, but comes with focus handling and ESC to close, all rolled in. Admittedly, styling <dialog>` can vary slightly across browsers, but it’s far less painful than fully creating your own modal component.

3. Progress Bars

If you’ve ever styled a div to look like a progress bar, you’re over-engineering, and a subtle change to the progress element would make it more accessible, too.

<progress value="70" max="100"></progress>

You can still style it with CSS, and pass values as props in the normal way.

<progress value={uploadPercent} max="100" />

4. Form Controls

Look around the web and you’ll often see div’s and all kinds of elements used to hack together form controls that can be handled with <input type="date">, <input type="color">, and <input type="range">.

Developers love to reinvent form components, usually because native controls look different on every OS, and developers like to have control over the visuals, even, unfortunately, at the cost of accessibility.

The trade off is between design control and built-in validation handling, keyboard input, and accessibility like semantic meaning, keyboard support and more.

<label>
  Choose a color:
  <input type="color" value="#FF0000">
</label>

<label>
  Pick a date:
  <input type="date">
</label>

Admittedly there’s less of a saving here, as React handles these natively too, and you can make them controlled components easily, too.

5. Autocomplete

Autocomplete inputs are another classic over-engineering trap. People reach for full UI libraries, but the browser has a native option.

<input list="fruits" placeholder="Choose a fruit...">
<datalist id="fruits">
  <option value="Apple">
  <option value="Banana">
  <option value="Cherry">
</datalist>

It even works in React:

<input list="fruits" value={fruit} onChange={e => setFruit(e.target.value)} />
<datalist id="fruits">
  {options.map(o => <option key={o} value={o} />)}
</datalist>

It’s a lightweight and accessible way to add auto-complete without over-complicating your code.

It’s worth bearing in mind, you’re a little limited with styling datalist elements, and can’t do async fetching, so your suggestions would need to already exist in the DOM. If they don’t, stick to the framework approach.

Honorable Mentions

There’s a few other fun elements we might pick these up in an article in future that we’d recommend you check out before reaching for something more complex:

  • <output>: this displays calculated results linked to form inputs, and is massively overlooked.
  • <fieldset> + <legend>: these groups form sections semantically.
  • <picture>: useful for making responsive images without a single line of JS.
  • contenteditable: makes any element instantly editable.

Modern browsers are seriously capable, and most of these native elements work in frameworks, with the only extra step often being reaching for a ref), or accepting that native controls look slightly different on each platform in exchange for accessibility.

So, next time why not take a look at native elements and see what they can do, because the chances are, it’s more than you realise.

Other interesting articles:
React
Vue.js
Angular
HTML
CSS
JavaScript
See all articles