Become a member!

Native HTML Features That Can Replace Your JavaScript: Popover, Details, and Datalist

🌐
This article is also available in other languages:
🇮🇹 Italiano  •  🇪🇸 Español

TL;DR - Key Takeaways

  • Popover API: Create dropdown menus, tooltips, and modal-like overlays with just HTML attributes (popover, popovertarget). The browser handles click-outside detection, focus management, keyboard navigation, and z-index automatically.

  • Details/Summary: Build accessible accordions without JavaScript. Use the name attribute to create exclusive accordions where only one section can be open at a time.

  • Datalist: Add autocomplete suggestions to any input field by linking it to a <datalist> element. Works with text, email, number, color, and date inputs.

  • Browser Support: All three features have 90%+ global browser support and work in Chrome, Firefox, Safari, and Edge.

  • When to use: Start with native HTML for common UI patterns. Only reach for JavaScript when you need rich customization, complex animations, or very large datasets.

Every web developer has been there. You need a simple dropdown menu, an accordion for your FAQ page, or an autocomplete field for a search input. The feature seems straightforward, so you start coding. A few hours later, you’re knee-deep in state management, event handlers, ARIA attributes, and edge cases you never anticipated. What started as a “quick feature” has become a maintenance burden that will haunt you for months.

The irony? The browser can handle most of this for you. Native HTML has evolved dramatically, and many common UI patterns now have built-in solutions that are more accessible, more performant, and significantly less prone to bugs than custom JavaScript implementations.

In this guide, we’ll explore three powerful HTML features that can replace substantial amounts of JavaScript code: the Popover API, the Details/Summary elements, and the Datalist element. These aren’t experimental features hidden behind flags - they’re production-ready solutions supported across modern browsers.

The Hidden Cost of Custom UI Components

Before we dive into the solutions, let’s acknowledge the real problem. When you implement a custom dropdown menu in JavaScript, you’re not just writing code to show and hide content. You’re taking responsibility for:

  • Click-outside detection: When the user clicks anywhere outside your menu, it should close. Sounds simple, but edge cases multiply quickly. What about clicks on nested elements? What about clicks that start inside but end outside during a drag?

  • Keyboard navigation: Users expect to close menus with the Escape key. Screen reader users expect proper focus management. Keyboard-only users need to navigate without a mouse.

  • Focus trapping: When a menu is open, focus shouldn’t escape to elements behind it. But it also shouldn’t trap users indefinitely.

  • Z-index management: Your menu needs to appear above everything else. But what happens when another component also uses high z-index values? Welcome to z-index wars.

  • Scroll behavior: What happens when the user scrolls while your menu is open? What about on mobile when the virtual keyboard appears?

  • State coordination: If the user opens a second menu, should the first one close automatically?

Each of these concerns requires code. Each line of code can have bugs. Each bug creates a support ticket. Each ticket takes time away from building features that actually differentiate your product.

The browser, on the other hand, has been solving these problems for decades. Browser developers have encountered every edge case, handled every accessibility requirement, and optimized every performance bottleneck. When you use native HTML features, you’re leveraging that collective expertise.


The Popover API: Native Dropdowns, Menus, and Tooltips

Quick Reference: Popover API

Attribute Purpose Example
popover Makes element a popover <div popover>...</div>
popover="auto" Auto-dismiss on click outside Default behavior
popover="manual" Only closes programmatically For tooltips
popovertarget="id" Button triggers popover <button popovertarget="menu">
popovertargetaction hide, show, or toggle popovertargetaction="hide"

Browser Support: Chrome 114+, Firefox 125+, Safari 17+, Edge 114+ (~90% global)

The Popover API is one of the most significant additions to HTML in recent years. It provides a declarative way to create content that appears “above” the rest of the page - perfect for dropdown menus, tooltips, notification panels, and similar UI patterns.

The Problem It Solves

Traditional dropdown implementations require:

// The "simple" dropdown - what it actually takes
function setupDropdown(buttonId, menuId) {
  const button = document.getElementById(buttonId);
  const menu = document.getElementById(menuId);
  let isOpen = false;

  // Toggle on button click
  button.addEventListener('click', (e) => {
    e.stopPropagation();
    isOpen = !isOpen;
    menu.style.display = isOpen ? 'block' : 'none';
    button.setAttribute('aria-expanded', isOpen);
    if (isOpen) {
      menu.querySelector('a, button')?.focus();
    }
  });

  // Close on outside click
  document.addEventListener('click', (e) => {
    if (isOpen && !menu.contains(e.target) && e.target !== button) {
      isOpen = false;
      menu.style.display = 'none';
      button.setAttribute('aria-expanded', 'false');
    }
  });

  // Close on Escape key
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && isOpen) {
      isOpen = false;
      menu.style.display = 'none';
      button.setAttribute('aria-expanded', 'false');
      button.focus();
    }
  });

  // Handle focus trap... (more code)
  // Handle z-index... (more code)
  // Handle multiple dropdowns... (more code)
}

This is the simplified version. Real-world implementations are much more complex.

The Native Solution

With the Popover API, the same functionality requires:

<button popovertarget="user-menu">Account</button>

<div id="user-menu" popover>
  <a href="/profile">Profile</a>
  <a href="/settings">Settings</a>
  <button popovertarget="user-menu" popovertargetaction="hide">
    Sign Out
  </button>
</div>

That’s it. No JavaScript. The browser handles:

  • Showing and hiding the popover when the button is clicked
  • Closing the popover when clicking outside (light dismiss)
  • Closing the popover when pressing Escape
  • Proper focus management
  • Promoting the popover to the top layer (no z-index needed)
  • Ensuring only one auto popover is open at a time

Try the live demo on CodePen

Understanding Popover Modes

The popover attribute accepts different values that control behavior:

Auto mode (default):

<div popover="auto" id="my-popup">
  This closes when you click outside or press Escape.
  Opening another auto popover closes this one.
</div>

<!-- Equivalent to: -->
<div popover id="my-popup">...</div>

Manual mode:

<div popover="manual" id="my-panel">
  This stays open until explicitly closed.
  Multiple manual popovers can be open simultaneously.
</div>

Manual mode is perfect for persistent panels, sidebars, or notifications that shouldn’t disappear on outside clicks.

Declarative Control with popovertargetaction

The popovertargetaction attribute lets you specify exactly what a button should do:

<!-- Toggle (default) - opens if closed, closes if open -->
<button popovertarget="menu">Toggle Menu</button>

<!-- Show only - does nothing if already open -->
<button popovertarget="menu" popovertargetaction="show">Open Menu</button>

<!-- Hide only - does nothing if already closed -->
<button popovertarget="menu" popovertargetaction="hide">Close Menu</button>

This is particularly useful for having an “X” close button inside your popover:

<div id="notification" popover>
  <p>You have new messages!</p>
  <button popovertarget="notification" popovertargetaction="hide">
    Dismiss
  </button>
</div>

Complete User Menu Example

Here’s a production-ready user menu implementation:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Popover Menu Example</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }

    body {
      font-family: system-ui, -apple-system, sans-serif;
      background: #1a1a2e;
      color: #eee;
      min-height: 100vh;
    }

    header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 1rem 2rem;
      background: rgba(255,255,255,0.05);
    }

    .logo { font-weight: 700; font-size: 1.25rem; }

    .avatar-btn {
      width: 44px;
      height: 44px;
      border-radius: 50%;
      border: 2px solid rgba(255,255,255,0.2);
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      font-weight: 700;
      font-size: 1rem;
      cursor: pointer;
      transition: border-color 0.2s;
    }

    .avatar-btn:hover {
      border-color: rgba(255,255,255,0.5);
    }

    .user-menu {
      margin: 0;
      padding: 0.5rem;
      min-width: 220px;
      border: 1px solid rgba(255,255,255,0.1);
      border-radius: 12px;
      background: #2a2a4a;
      box-shadow: 0 20px 60px rgba(0,0,0,0.4);
    }

    .user-menu::backdrop {
      background: transparent;
    }

    .menu-header {
      padding: 0.75rem 1rem;
      border-bottom: 1px solid rgba(255,255,255,0.1);
      font-size: 0.875rem;
      opacity: 0.7;
    }

    .menu-item {
      display: block;
      width: 100%;
      padding: 0.75rem 1rem;
      border: none;
      border-radius: 8px;
      background: transparent;
      color: inherit;
      text-decoration: none;
      text-align: left;
      font-size: 1rem;
      cursor: pointer;
      transition: background 0.15s;
    }

    .menu-item:hover {
      background: rgba(255,255,255,0.1);
    }

    .menu-item.danger:hover {
      background: rgba(255,59,48,0.2);
      color: #ff6b6b;
    }
  </style>
</head>
<body>
  <header>
    <div class="logo">MyApp</div>
    <button class="avatar-btn"
            popovertarget="user-menu"
            aria-label="Open user menu">
      DT
    </button>
  </header>

  <div id="user-menu" popover class="user-menu">
    <div class="menu-header">Signed in as Daniele</div>
    <a href="/profile" class="menu-item">Profile</a>
    <a href="/settings" class="menu-item">Settings</a>
    <a href="/billing" class="menu-item">Billing</a>
    <hr style="border-color: rgba(255,255,255,0.1); margin: 0.5rem 0;">
    <button class="menu-item danger"
            popovertarget="user-menu"
            popovertargetaction="hide">
      Sign Out
    </button>
  </div>
</body>
</html>

Save this as an HTML file and open it in your browser. Click the avatar button to see the menu. Click outside or press Escape to close it. Notice how smooth and reliable it is - with zero JavaScript.

JavaScript Integration When Needed

Sometimes you need JavaScript for product-specific behavior. The Popover API provides methods and events for this:

const popover = document.getElementById('my-popover');

// Programmatic control
popover.showPopover();   // Show the popover
popover.hidePopover();   // Hide the popover
popover.togglePopover(); // Toggle visibility

// Check if open
if (popover.matches(':popover-open')) {
  console.log('Popover is visible');
}

// Listen for state changes
popover.addEventListener('toggle', (event) => {
  console.log(`Popover is now: ${event.newState}`); // 'open' or 'closed'

  if (event.newState === 'open') {
    // Analytics, lazy-load content, etc.
  }
});

The key insight is that JavaScript should add product behavior, not recreate platform behavior. Let the browser handle showing, hiding, focus management, and keyboard interaction. Use JavaScript only for what makes your application unique.

Tooltip Implementation

Popovers are perfect for tooltips. Here’s a simple hover-triggered tooltip pattern:

<span class="tooltip-container">
  <button id="help-btn"
          popovertarget="help-tooltip"
          popovertargetaction="toggle">
    ?
  </button>
  <div id="help-tooltip" popover="manual" class="tooltip">
    This field accepts your company email address.
    Contact IT if you need assistance.
  </div>
</span>

<style>
  .tooltip-container {
    position: relative;
    display: inline-block;
  }

  .tooltip {
    margin: 0;
    padding: 0.5rem 0.75rem;
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    background: #333;
    color: white;
    font-size: 0.875rem;
    border-radius: 6px;
    white-space: nowrap;
    max-width: 250px;
    white-space: normal;
  }

  .tooltip::before {
    content: '';
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    border: 6px solid transparent;
    border-bottom-color: #333;
  }
</style>

<script>
  const btn = document.getElementById('help-btn');
  const tooltip = document.getElementById('help-tooltip');

  btn.addEventListener('mouseenter', () => tooltip.showPopover());
  btn.addEventListener('mouseleave', () => tooltip.hidePopover());
  btn.addEventListener('focus', () => tooltip.showPopover());
  btn.addEventListener('blur', () => tooltip.hidePopover());
</script>

Try the live demo on CodePen

Using popover="manual" ensures the tooltip only shows when we explicitly control it via JavaScript, rather than auto-dismissing on outside clicks.

Notification Toast

Create dismissible notifications that stack properly:

<button onclick="showNotification()">Show Notification</button>

<div id="notification" popover="manual" class="notification success">
  <span class="notification-icon">✓</span>
  <span class="notification-message">Your changes have been saved!</span>
  <button popovertarget="notification"
          popovertargetaction="hide"
          class="notification-close">
    &times;
  </button>
</div>

<style>
  .notification {
    margin: 0;
    padding: 1rem 1.25rem;
    position: fixed;
    bottom: 1rem;
    right: 1rem;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    min-width: 300px;
    border: none;
    border-radius: 8px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.2);
    animation: slideIn 0.3s ease;
  }

  .notification.success {
    background: #d4edda;
    border-left: 4px solid #28a745;
  }

  .notification.error {
    background: #f8d7da;
    border-left: 4px solid #dc3545;
  }

  .notification-close {
    margin-left: auto;
    background: none;
    border: none;
    font-size: 1.25rem;
    cursor: pointer;
    opacity: 0.5;
  }

  .notification-close:hover {
    opacity: 1;
  }

  @keyframes slideIn {
    from {
      transform: translateX(100%);
      opacity: 0;
    }
    to {
      transform: translateX(0);
      opacity: 1;
    }
  }
</style>

<script>
  function showNotification() {
    const notification = document.getElementById('notification');
    notification.showPopover();

    // Auto-dismiss after 5 seconds
    setTimeout(() => {
      if (notification.matches(':popover-open')) {
        notification.hidePopover();
      }
    }, 5000);
  }
</script>

Try the live demo on CodePen

Mobile Navigation Drawer

Popovers work excellently for slide-out navigation on mobile:

<button class="hamburger"
        popovertarget="mobile-nav"
        popovertargetaction="show"
        aria-label="Open navigation">
  <span></span>
  <span></span>
  <span></span>
</button>

<nav id="mobile-nav" popover="manual" class="drawer">
  <div class="drawer-header">
    <span class="drawer-title">Navigation</span>
    <button popovertarget="mobile-nav"
            popovertargetaction="hide"
            aria-label="Close navigation">
      &times;
    </button>
  </div>
  <a href="/" class="drawer-link">Home</a>
  <a href="/products" class="drawer-link">Products</a>
  <a href="/about" class="drawer-link">About</a>
  <a href="/contact" class="drawer-link">Contact</a>
</nav>

<style>
  .drawer {
    margin: 0;
    padding: 0;
    position: fixed;
    inset: 0 auto 0 0;
    width: min(85vw, 320px);
    height: 100vh;
    border: none;
    background: #1a1a2e;
    transform: translateX(-100%);
    transition: transform 0.3s ease;
  }

  .drawer:popover-open {
    transform: translateX(0);
  }

  .drawer::backdrop {
    background: rgba(0,0,0,0.5);
    opacity: 0;
    transition: opacity 0.3s;
  }

  .drawer:popover-open::backdrop {
    opacity: 1;
  }
</style>

We use popover="manual" here because we want explicit control over when it closes - users on mobile expect to tap a close button or navigation link, not have the drawer disappear when they accidentally tap near the edge.

Browser Support for Popover

The Popover API is supported in all modern browsers since April 2024. Check the current compatibility status:

Can I Use: Popover API - Interactive browser support table

For older browsers, treat popover as progressive enhancement: provide a fallback that shows the content inline or use a polyfill.


Details and Summary: Native Accordions Without the Headaches

Quick Reference: Details/Summary Elements

Element/Attribute Purpose Example
<details> Container for collapsible content <details>...</details>
<summary> Clickable header/label <summary>Click me</summary>
open attribute Pre-expand the section <details open>
name attribute Create exclusive accordion groups <details name="faq">
toggle event Fires when opened/closed details.addEventListener('toggle', ...)

Browser Support: Details/Summary 97%+ global; name attribute 85%+ (Chrome 120+, Firefox 130+, Safari 17.2+)

The <details> and <summary> elements create disclosure widgets - UI components that expand and collapse to show or hide content. They’re perfect for FAQs, expandable sections, collapsible sidebars, and yes, accordions.

The Problem with Custom Accordions

A typical JavaScript accordion implementation involves:

// Accordion component - the "simple" version
class Accordion {
  constructor(container) {
    this.container = container;
    this.panels = container.querySelectorAll('.panel');
    this.bindEvents();
  }

  bindEvents() {
    this.panels.forEach(panel => {
      const header = panel.querySelector('.header');
      header.addEventListener('click', () => this.toggle(panel));
      header.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          this.toggle(panel);
        }
      });
    });
  }

  toggle(panel) {
    const isOpen = panel.classList.contains('open');

    // Close all panels (for exclusive accordion)
    this.panels.forEach(p => {
      p.classList.remove('open');
      p.querySelector('.header').setAttribute('aria-expanded', 'false');
      p.querySelector('.content').setAttribute('aria-hidden', 'true');
    });

    // Open clicked panel if it was closed
    if (!isOpen) {
      panel.classList.add('open');
      panel.querySelector('.header').setAttribute('aria-expanded', 'true');
      panel.querySelector('.content').setAttribute('aria-hidden', 'false');
    }
  }
}

Plus you need the correct HTML structure with all the ARIA attributes, CSS for animations, and careful attention to accessibility requirements. It’s a lot of code for a simple “expand/collapse” behavior.

The Native Solution

<details>
  <summary>What is your return policy?</summary>
  <p>We offer a 30-day return policy on all items. Items must be
  in original condition with tags attached.</p>
</details>

The browser handles everything:

  • Click to expand/collapse
  • Keyboard accessibility (Enter and Space toggle)
  • Proper ARIA roles (implicitly)
  • Screen reader announcements
  • Focus management

Try the live demo on CodePen

Creating Exclusive Accordions with the name Attribute

The name attribute is a relatively recent addition that enables exclusive accordions - where opening one section automatically closes the others:

<section class="faq" aria-label="Frequently Asked Questions">
  <details name="faq-group" open>
    <summary>How do I create an account?</summary>
    <div class="answer">
      <p>Click the "Sign Up" button in the top right corner.
      Fill in your email and choose a password.
      You'll receive a confirmation email within minutes.</p>
    </div>
  </details>

  <details name="faq-group">
    <summary>How do I reset my password?</summary>
    <div class="answer">
      <p>Click "Forgot Password" on the login page.
      Enter your email address, and we'll send you a reset link.
      The link expires after 24 hours.</p>
    </div>
  </details>

  <details name="faq-group">
    <summary>Can I change my username?</summary>
    <div class="answer">
      <p>Yes! Go to Settings > Profile > Edit Username.
      Note that you can only change your username once every 30 days.</p>
    </div>
  </details>

  <details name="faq-group">
    <summary>How do I contact support?</summary>
    <div class="answer">
      <p>You can reach our support team via email at support@example.com
      or through the live chat widget available on every page.
      We typically respond within 2 hours during business hours.</p>
    </div>
  </details>
</section>

All <details> elements with the same name value form a group. When you open one, the browser automatically closes the others. No JavaScript required.

Try the live demo on CodePen

Styling Details Elements

The default styling is functional but plain. Here’s how to create a polished accordion:

.faq {
  max-width: 700px;
  margin: 2rem auto;
}

.faq details {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  margin-bottom: 0.75rem;
  background: #fff;
  overflow: hidden;
}

.faq summary {
  padding: 1rem 1.25rem;
  font-weight: 600;
  cursor: pointer;
  list-style: none; /* Remove default marker */
  display: flex;
  justify-content: space-between;
  align-items: center;
  transition: background 0.2s;
}

.faq summary:hover {
  background: #f5f5f5;
}

/* Remove default marker in Safari */
.faq summary::-webkit-details-marker {
  display: none;
}

/* Custom expand/collapse indicator */
.faq summary::after {
  content: '+';
  font-size: 1.5rem;
  font-weight: 300;
  color: #666;
  transition: transform 0.2s;
}

.faq details[open] summary::after {
  content: '−';
}

/* Optional: rotate icon instead of changing content */
/*
.faq summary::after {
  content: '▸';
  transition: transform 0.2s;
}

.faq details[open] summary::after {
  transform: rotate(90deg);
}
*/

.faq details[open] summary {
  border-bottom: 1px solid #e0e0e0;
}

.faq .answer {
  padding: 1rem 1.25rem;
  line-height: 1.6;
  color: #444;
}

Adding Smooth Animations

Native <details> elements don’t animate by default, but you can add smooth transitions with a bit of CSS:

.faq details {
  /* ... other styles ... */
}

.faq .answer {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s ease;
}

.faq details[open] .answer {
  grid-template-rows: 1fr;
}

.faq .answer > * {
  overflow: hidden;
}

This uses the CSS Grid technique for animating height from 0 to auto - something that’s traditionally been impossible with CSS alone.

Handling the toggle Event

When you need to respond to open/close events:

const details = document.querySelector('details');

details.addEventListener('toggle', (event) => {
  if (details.open) {
    console.log('Panel opened');
    // Track analytics, load content, etc.
  } else {
    console.log('Panel closed');
  }
});

Collapsible Code Snippets

Details/Summary is excellent for showing code examples that users can expand when needed:

<article class="tutorial">
  <h3>Connecting to the Database</h3>
  <p>First, install the required package and configure your connection string.</p>

  <details class="code-block">
    <summary>
      <span class="code-label">database.js</span>
      <span class="code-lang">JavaScript</span>
    </summary>
    <pre><code>const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.DB_HOST,
  port: process.env.DB_PORT || 5432,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  ssl: process.env.NODE_ENV === 'production'
});

module.exports = { pool };</code></pre>
  </details>
</article>

<style>
  .code-block {
    margin: 1rem 0;
    border: 1px solid #e1e4e8;
    border-radius: 6px;
    overflow: hidden;
  }

  .code-block summary {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0.75rem 1rem;
    background: #f6f8fa;
    cursor: pointer;
    list-style: none;
  }

  .code-block summary::-webkit-details-marker {
    display: none;
  }

  .code-label {
    font-family: monospace;
    font-weight: 600;
  }

  .code-lang {
    font-size: 0.75rem;
    padding: 0.25rem 0.5rem;
    background: #e1e4e8;
    border-radius: 4px;
  }

  .code-block pre {
    margin: 0;
    padding: 1rem;
    background: #24292e;
    color: #e1e4e8;
    overflow-x: auto;
  }
</style>

Try the live demo on CodePen

Nested Details for Hierarchical Content

You can nest <details> elements to create tree-like structures for documentation or navigation:

<nav class="docs-nav" aria-label="Documentation">
  <details open>
    <summary>Getting Started</summary>
    <ul>
      <li><a href="/docs/installation">Installation</a></li>
      <li><a href="/docs/quickstart">Quick Start</a></li>
    </ul>
  </details>

  <details>
    <summary>Core Concepts</summary>
    <ul>
      <li><a href="/docs/components">Components</a></li>
      <li>
        <details>
          <summary>State Management</summary>
          <ul>
            <li><a href="/docs/state/local">Local State</a></li>
            <li><a href="/docs/state/global">Global State</a></li>
            <li><a href="/docs/state/async">Async State</a></li>
          </ul>
        </details>
      </li>
      <li><a href="/docs/routing">Routing</a></li>
    </ul>
  </details>

  <details>
    <summary>API Reference</summary>
    <ul>
      <li><a href="/api/hooks">Hooks</a></li>
      <li><a href="/api/utilities">Utilities</a></li>
    </ul>
  </details>
</nav>

Try the live demo on CodePen

This pattern is particularly useful for documentation sidebars, file browsers, or any hierarchical data that benefits from progressive disclosure.

Practical Example: Deep-Linking to Open Sections

A common requirement is to link directly to an open accordion panel:

<details id="shipping" name="faq">
  <summary>Shipping Information</summary>
  <p>We ship worldwide. Standard shipping takes 5-7 business days.</p>
</details>

<details id="returns" name="faq">
  <summary>Returns &amp; Refunds</summary>
  <p>30-day return policy on all items.</p>
</details>

<script>
  // Open the section specified in the URL hash
  function openFromHash() {
    const hash = window.location.hash.slice(1);
    if (hash) {
      const target = document.getElementById(hash);
      if (target && target.tagName === 'DETAILS') {
        target.open = true;
        target.scrollIntoView({ behavior: 'smooth' });
      }
    }
  }

  // Run on page load
  openFromHash();

  // Update URL when sections are toggled
  document.querySelectorAll('details[name="faq"]').forEach(details => {
    details.addEventListener('toggle', () => {
      if (details.open) {
        history.replaceState(null, '', `#${details.id}`);
      }
    });
  });
</script>

Now users can share URLs like https://example.com/faq#returns that open directly to the relevant section.

Browser Support for Details/Summary

The <details> and <summary> elements have been widely supported since 2020. The name attribute for exclusive accordions is newer - supported in Chrome 120+, Safari 17.2+, and Firefox 130+. For older browsers, the accordion will still work, but multiple panels can be open simultaneously.

Check the current compatibility status:


Datalist: Native Autocomplete Without Dependencies

Quick Reference: Datalist Element

Feature Syntax Description
Basic usage <input list="id"> + <datalist id="id"> Links input to suggestions
Options <option value="..."> Each suggestion
With labels <option value="val" label="Display"> Show label, submit value
Input types text, email, url, tel, number, range, color, date, time All support datalist

Key difference from <select>: Users can type custom values not in the list

Browser Support: 96%+ global (all modern browsers)

The <datalist> element provides a list of suggested values for an input field. It’s the native solution for autocomplete dropdowns, type-ahead suggestions, and combobox patterns.

The Problem with Custom Autocomplete

Autocomplete implementations are notoriously complex:

// A "simple" autocomplete - abbreviated
class Autocomplete {
  constructor(input, options) {
    this.input = input;
    this.options = options;
    this.dropdown = this.createDropdown();
    this.selectedIndex = -1;
    this.bindEvents();
  }

  createDropdown() {
    const dropdown = document.createElement('div');
    dropdown.className = 'autocomplete-dropdown';
    dropdown.setAttribute('role', 'listbox');
    this.input.parentNode.appendChild(dropdown);
    return dropdown;
  }

  bindEvents() {
    this.input.addEventListener('input', () => this.onInput());
    this.input.addEventListener('keydown', (e) => this.onKeydown(e));
    this.input.addEventListener('blur', () => this.hideDropdown());
    document.addEventListener('click', (e) => this.onOutsideClick(e));
  }

  onInput() {
    const value = this.input.value.toLowerCase();
    const matches = this.options.filter(opt =>
      opt.toLowerCase().includes(value)
    );
    this.renderDropdown(matches);
  }

  onKeydown(e) {
    switch(e.key) {
      case 'ArrowDown':
        e.preventDefault();
        this.selectNext();
        break;
      case 'ArrowUp':
        e.preventDefault();
        this.selectPrevious();
        break;
      case 'Enter':
        e.preventDefault();
        this.confirmSelection();
        break;
      case 'Escape':
        this.hideDropdown();
        break;
    }
  }

  // ... many more methods for rendering, selection, positioning, etc.
}

Custom implementations need to handle keyboard navigation, focus management, positioning, scrolling within the dropdown, screen reader announcements, and much more.

The Native Solution

<label for="country">Country:</label>
<input type="text" id="country" name="country" list="countries">

<datalist id="countries">
  <option value="Argentina">
  <option value="Australia">
  <option value="Austria">
  <option value="Belgium">
  <option value="Brazil">
  <option value="Canada">
  <option value="France">
  <option value="Germany">
  <option value="Italy">
  <option value="Japan">
  <option value="Spain">
  <option value="United Kingdom">
  <option value="United States">
</datalist>

That’s the entire implementation. The browser provides:

  • A dropdown with matching suggestions
  • Keyboard navigation (arrow keys, Enter to select)
  • Filtering as the user types
  • Native accessibility
  • Mobile-optimized UI

Try the live demo on CodePen

Key Characteristics of Datalist

Unlike <select>, the datalist approach:

  1. Allows free-form input: Users can type anything, not just the predefined options
  2. Shows suggestions based on input: The list filters as the user types
  3. Is progressive enhancement: If datalist isn’t supported, the input still works as a regular text field

Using Datalist with Different Input Types

Datalist works with many input types, not just text:

Email suggestions:

<input type="email" list="email-domains">
<datalist id="email-domains">
  <option value="@gmail.com">
  <option value="@outlook.com">
  <option value="@yahoo.com">
  <option value="@icloud.com">
</datalist>

URL suggestions:

<input type="url" list="common-urls">
<datalist id="common-urls">
  <option value="https://github.com/">
  <option value="https://stackoverflow.com/">
  <option value="https://developer.mozilla.org/">
</datalist>

Range input with tick marks:

<label>
  Rating:
  <input type="range" min="0" max="10" list="ratings">
</label>
<datalist id="ratings">
  <option value="0" label="Poor">
  <option value="5" label="Average">
  <option value="10" label="Excellent">
</datalist>

Color picker with presets:

<label>
  Brand Color:
  <input type="color" list="brand-colors">
</label>
<datalist id="brand-colors">
  <option value="#FF5733" label="Sunset Orange">
  <option value="#3498DB" label="Ocean Blue">
  <option value="#2ECC71" label="Emerald Green">
  <option value="#9B59B6" label="Royal Purple">
  <option value="#F39C12" label="Golden Yellow">
</datalist>

Try the live demo on CodePen

Search Box with Recent Searches

A common pattern is showing recent search history as suggestions:

<div class="search-container">
  <input type="search"
         id="search-input"
         list="recent-searches"
         placeholder="Search products..."
         autocomplete="off">
  <datalist id="recent-searches">
    <!-- Populated from localStorage or API -->
  </datalist>
</div>

<script>
  const searchInput = document.getElementById('search-input');
  const datalist = document.getElementById('recent-searches');

  // Load recent searches from localStorage
  function loadRecentSearches() {
    const searches = JSON.parse(localStorage.getItem('recentSearches') || '[]');
    datalist.innerHTML = searches
      .slice(0, 5) // Show last 5 searches
      .map(s => `<option value="${s}">`)
      .join('');
  }

  // Save search when user submits
  function saveSearch(query) {
    if (!query.trim()) return;

    let searches = JSON.parse(localStorage.getItem('recentSearches') || '[]');
    searches = [query, ...searches.filter(s => s !== query)].slice(0, 10);
    localStorage.setItem('recentSearches', JSON.stringify(searches));
    loadRecentSearches();
  }

  loadRecentSearches();

  // Save on Enter key or form submit
  searchInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') {
      saveSearch(searchInput.value);
    }
  });
</script>

Try the live demo on CodePen

Time Picker with Common Times

Datalist works great for time inputs with preset options:

<label for="meeting-time">Schedule meeting:</label>
<input type="time"
       id="meeting-time"
       list="common-times"
       min="09:00"
       max="18:00">

<datalist id="common-times">
  <option value="09:00" label="9:00 AM">
  <option value="09:30" label="9:30 AM">
  <option value="10:00" label="10:00 AM">
  <option value="10:30" label="10:30 AM">
  <option value="11:00" label="11:00 AM">
  <option value="11:30" label="11:30 AM">
  <option value="12:00" label="12:00 PM - Noon">
  <option value="13:00" label="1:00 PM">
  <option value="14:00" label="2:00 PM">
  <option value="15:00" label="3:00 PM">
  <option value="16:00" label="4:00 PM">
  <option value="17:00" label="5:00 PM">
</datalist>

Try the live demo on CodePen

Number Input with Suggested Values

For numeric inputs where certain values are common:

<label for="quantity">Quantity:</label>
<input type="number"
       id="quantity"
       list="common-quantities"
       min="1"
       max="1000"
       value="1">

<datalist id="common-quantities">
  <option value="1">
  <option value="5">
  <option value="10">
  <option value="25">
  <option value="50">
  <option value="100">
</datalist>

Try the live demo on CodePen

Dynamic Datalist with JavaScript

For autocomplete that needs to fetch suggestions from an API:

<input type="text"
       id="search"
       list="search-suggestions"
       placeholder="Search products...">
<datalist id="search-suggestions"></datalist>

<script>
  const input = document.getElementById('search');
  const datalist = document.getElementById('search-suggestions');
  let debounceTimer;

  input.addEventListener('input', () => {
    clearTimeout(debounceTimer);

    if (input.value.length < 2) {
      datalist.innerHTML = '';
      return;
    }

    debounceTimer = setTimeout(async () => {
      try {
        const response = await fetch(
          `/api/suggestions?q=${encodeURIComponent(input.value)}`
        );
        const suggestions = await response.json();

        datalist.innerHTML = suggestions
          .map(s => `<option value="${escapeHtml(s)}">`)
          .join('');
      } catch (error) {
        console.error('Failed to fetch suggestions:', error);
      }
    }, 300);
  });

  function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }
</script>

When to Use Datalist vs. Custom Autocomplete

Use datalist when:

  • You need simple suggestions from a predefined or dynamic list
  • Visual customization isn’t critical
  • You want maximum accessibility with minimal code
  • The list of options is reasonable in size (dozens, not thousands)

Use a custom autocomplete when:

  • You need rich content in suggestions (images, descriptions, categories)
  • You require fine-grained control over styling
  • You’re building a search box with advanced features (highlighting, grouping)
  • You need to handle very large datasets with virtualization

Accessibility Considerations

While datalist is generally accessible, be aware of some limitations:

  • The dropdown appearance is browser-controlled and can’t be fully styled
  • Some screen reader + browser combinations don’t announce suggestions perfectly
  • Font size in the dropdown doesn’t scale with page zoom

For critical functionality, always test with your target audience and assistive technologies.

Browser Support for Datalist

The <datalist> element is well supported across all modern browsers. Check the current compatibility status:

Can I Use: Datalist element - Interactive browser support table (96%+ global support)

Note that while basic support is excellent, behavior can vary slightly between browsers - particularly on mobile devices. Always test your specific use case.


Combining These Features: A Complete Example

Let’s build a practical example that combines all three features - a settings panel with a popover, accordion sections, and autocomplete inputs:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Settings Panel - Native HTML Features</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    body {
      font-family: system-ui, -apple-system, sans-serif;
      background: #f5f5f7;
      min-height: 100vh;
      padding: 2rem;
    }

    .container {
      max-width: 600px;
      margin: 0 auto;
    }

    h1 {
      margin-bottom: 1.5rem;
      color: #1d1d1f;
    }

    /* Settings Button */
    .settings-btn {
      display: inline-flex;
      align-items: center;
      gap: 0.5rem;
      padding: 0.75rem 1.25rem;
      background: #007aff;
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 1rem;
      cursor: pointer;
      transition: background 0.2s;
    }

    .settings-btn:hover {
      background: #0056b3;
    }

    /* Settings Panel (Popover) */
    .settings-panel {
      margin: 0;
      padding: 1.5rem;
      width: min(90vw, 500px);
      border: none;
      border-radius: 16px;
      background: white;
      box-shadow: 0 20px 60px rgba(0,0,0,0.15);
    }

    .settings-panel::backdrop {
      background: rgba(0,0,0,0.3);
    }

    .panel-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 1.5rem;
      padding-bottom: 1rem;
      border-bottom: 1px solid #e5e5e5;
    }

    .panel-header h2 {
      font-size: 1.25rem;
      color: #1d1d1f;
    }

    .close-btn {
      width: 32px;
      height: 32px;
      border: none;
      border-radius: 50%;
      background: #f5f5f5;
      font-size: 1.25rem;
      cursor: pointer;
      transition: background 0.2s;
    }

    .close-btn:hover {
      background: #e5e5e5;
    }

    /* Accordion Sections */
    .settings-section {
      border: 1px solid #e5e5e5;
      border-radius: 12px;
      margin-bottom: 0.75rem;
      overflow: hidden;
    }

    .settings-section summary {
      padding: 1rem;
      font-weight: 600;
      cursor: pointer;
      list-style: none;
      display: flex;
      justify-content: space-between;
      align-items: center;
      background: #fafafa;
      transition: background 0.2s;
    }

    .settings-section summary:hover {
      background: #f0f0f0;
    }

    .settings-section summary::-webkit-details-marker {
      display: none;
    }

    .settings-section summary::after {
      content: '▸';
      transition: transform 0.2s;
    }

    .settings-section[open] summary::after {
      transform: rotate(90deg);
    }

    .settings-section[open] summary {
      border-bottom: 1px solid #e5e5e5;
    }

    .section-content {
      padding: 1rem;
    }

    /* Form Styles */
    .form-group {
      margin-bottom: 1rem;
    }

    .form-group:last-child {
      margin-bottom: 0;
    }

    .form-group label {
      display: block;
      margin-bottom: 0.5rem;
      font-size: 0.875rem;
      font-weight: 500;
      color: #666;
    }

    .form-group input,
    .form-group select {
      width: 100%;
      padding: 0.75rem;
      border: 1px solid #ddd;
      border-radius: 8px;
      font-size: 1rem;
      transition: border-color 0.2s;
    }

    .form-group input:focus,
    .form-group select:focus {
      outline: none;
      border-color: #007aff;
    }

    /* Save Button */
    .save-btn {
      width: 100%;
      padding: 1rem;
      margin-top: 1rem;
      background: #007aff;
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 1rem;
      font-weight: 600;
      cursor: pointer;
      transition: background 0.2s;
    }

    .save-btn:hover {
      background: #0056b3;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>Dashboard</h1>

    <button class="settings-btn" popovertarget="settings-panel">
      <span>&#9881;</span> Settings
    </button>
  </div>

  <!-- Settings Panel Popover -->
  <div id="settings-panel" popover class="settings-panel">
    <div class="panel-header">
      <h2>Settings</h2>
      <button class="close-btn"
              popovertarget="settings-panel"
              popovertargetaction="hide"
              aria-label="Close settings">
        &times;
      </button>
    </div>

    <form id="settings-form">
      <!-- Profile Section -->
      <details class="settings-section" name="settings" open>
        <summary>Profile</summary>
        <div class="section-content">
          <div class="form-group">
            <label for="display-name">Display Name</label>
            <input type="text" id="display-name" name="displayName"
                   value="John Doe">
          </div>
          <div class="form-group">
            <label for="timezone">Timezone</label>
            <input type="text" id="timezone" name="timezone"
                   list="timezones" placeholder="Start typing...">
            <datalist id="timezones">
              <option value="America/New_York">
              <option value="America/Chicago">
              <option value="America/Denver">
              <option value="America/Los_Angeles">
              <option value="Europe/London">
              <option value="Europe/Paris">
              <option value="Europe/Rome">
              <option value="Europe/Berlin">
              <option value="Asia/Tokyo">
              <option value="Asia/Shanghai">
              <option value="Australia/Sydney">
            </datalist>
          </div>
        </div>
      </details>

      <!-- Notifications Section -->
      <details class="settings-section" name="settings">
        <summary>Notifications</summary>
        <div class="section-content">
          <div class="form-group">
            <label for="email-frequency">Email Frequency</label>
            <select id="email-frequency" name="emailFrequency">
              <option value="immediate">Immediate</option>
              <option value="daily">Daily Digest</option>
              <option value="weekly">Weekly Summary</option>
              <option value="never">Never</option>
            </select>
          </div>
        </div>
      </details>

      <!-- Appearance Section -->
      <details class="settings-section" name="settings">
        <summary>Appearance</summary>
        <div class="section-content">
          <div class="form-group">
            <label for="language">Language</label>
            <input type="text" id="language" name="language"
                   list="languages" value="English">
            <datalist id="languages">
              <option value="English">
              <option value="Español">
              <option value="Français">
              <option value="Deutsch">
              <option value="Italiano">
              <option value="Português">
              <option value="日本語">
              <option value="中文">
            </datalist>
          </div>
          <div class="form-group">
            <label for="theme-color">Accent Color</label>
            <input type="color" id="theme-color" name="themeColor"
                   list="theme-colors" value="#007aff">
            <datalist id="theme-colors">
              <option value="#007aff" label="Blue">
              <option value="#34c759" label="Green">
              <option value="#ff9500" label="Orange">
              <option value="#ff2d55" label="Pink">
              <option value="#5856d6" label="Purple">
            </datalist>
          </div>
        </div>
      </details>

      <button type="submit" class="save-btn">Save Changes</button>
    </form>
  </div>

  <script>
    // Handle form submission
    document.getElementById('settings-form').addEventListener('submit', (e) => {
      e.preventDefault();

      const formData = new FormData(e.target);
      const settings = Object.fromEntries(formData);

      console.log('Saving settings:', settings);

      // Close the panel after saving
      document.getElementById('settings-panel').hidePopover();

      // Show feedback (in production, use a toast notification)
      alert('Settings saved successfully!');
    });
  </script>
</body>
</html>

This example demonstrates:

  • A popover for the settings panel that handles show/hide, backdrop, and keyboard navigation
  • Details/Summary with the name attribute for exclusive accordion sections
  • Datalist for timezone, language, and color suggestions
  • Minimal JavaScript - only for form submission handling

When Native HTML Isn’t Enough

Despite the power of these native features, there are legitimate cases where custom JavaScript solutions make sense:

Popover limitations:

  • Complex positioning requirements (anchor positioning is still experimental)
  • Nested popovers with complex interaction patterns
  • Popovers that need to survive navigation in single-page apps

Details/Summary limitations:

  • Accordions that need to support multiple open panels with animations
  • Complex open/close animations beyond simple transitions
  • Nested accordions with shared state

Datalist limitations:

  • Rich suggestion rendering (images, descriptions, categories)
  • Large datasets requiring virtualization
  • Custom filtering logic or fuzzy matching
  • Full combobox patterns with create-new functionality

In these cases, reach for established libraries like Radix UI, Headless UI, or Downshift. But always ask yourself first: “Does the browser already do this?”


Conclusion

The browser platform has come a long way. Features that once required hundreds of lines of JavaScript and third-party dependencies are now available as native HTML attributes and elements.

The Popover API gives us dropdown menus, tooltips, and slide-out panels with proper focus management and accessibility built in. The Details and Summary elements provide accordions that work correctly out of the box, with the name attribute enabling exclusive behavior. The Datalist element offers autocomplete suggestions without the complexity of custom implementations.

By using these native features, you:

  • Reduce code complexity - less code means fewer bugs
  • Improve accessibility - browser implementations follow standards
  • Enhance performance - native features are optimized at the engine level
  • Simplify maintenance - the browser team handles edge cases

The philosophy is simple: let the browser handle behavior it already knows how to do. Save your JavaScript for the product logic that makes your application unique.

Start with native HTML. Reach for JavaScript only when you’ve exhausted what the platform provides. Your users, your team, and your future self will thank you.


Frequently Asked Questions (FAQ)

What is the HTML Popover API?

The Popover API is a native HTML feature that allows you to create overlay elements (like dropdown menus, tooltips, and dialogs) using simple HTML attributes. By adding popover to an element and popovertarget to a button, the browser automatically handles showing/hiding the element, click-outside dismissal, keyboard navigation (Escape to close), focus management, and z-index stacking. It’s supported in all modern browsers since 2024.

How do I create a dropdown menu without JavaScript?

Use the Popover API:

<button popovertarget="menu">Open Menu</button>
<div id="menu" popover>
  <a href="#">Option 1</a>
  <a href="#">Option 2</a>
</div>

The menu will appear when clicking the button and close automatically when clicking outside or pressing Escape.

What is the difference between popover=“auto” and popover=“manual”?

  • popover="auto" (default): The popover closes automatically when clicking outside or pressing Escape. Only one auto popover can be open at a time.
  • popover="manual": The popover only closes when explicitly triggered via JavaScript or another button with popovertargetaction="hide". Multiple manual popovers can be open simultaneously.

How do I create an accordion that only allows one section open at a time?

Use the name attribute on <details> elements:

<details name="faq" open>
  <summary>Question 1</summary>
  <p>Answer 1</p>
</details>
<details name="faq">
  <summary>Question 2</summary>
  <p>Answer 2</p>
</details>

All <details> elements with the same name value form an exclusive group - opening one automatically closes the others.

What is the datalist element used for?

The <datalist> element provides autocomplete suggestions for input fields. Unlike <select>, users can still type custom values - the suggestions are optional hints. It works with various input types including text, email, number, range, and color.

<input type="text" list="fruits">
<datalist id="fruits">
  <option value="Apple">
  <option value="Banana">
  <option value="Cherry">
</datalist>

Does the Popover API replace the dialog element?

No, they serve different purposes:

  • Popover: For non-modal overlays like menus, tooltips, and notifications. Users can still interact with the page.
  • Dialog: For modal interactions that require user attention before continuing. Use <dialog> with showModal() for forms, confirmations, and critical alerts.

What is CSS Anchor Positioning?

CSS Anchor Positioning is a CSS feature that allows you to position elements relative to an “anchor” element. It’s particularly useful with popovers to position dropdown menus directly below their trigger buttons:

.button { anchor-name: --my-anchor; }
.popover {
  position-anchor: --my-anchor;
  top: anchor(bottom);
  left: anchor(left);
}

This is supported in Chrome 125+ and other modern browsers.

Are these HTML features accessible?

Yes, these native HTML features are built with accessibility in mind:

  • Popover: Proper focus management, keyboard navigation, and ARIA attributes are handled automatically
  • Details/Summary: Screen readers announce the expanded/collapsed state
  • Datalist: Works with screen readers and keyboard navigation

However, always test with your target audience and assistive technologies for critical functionality.

What browsers support Popover, Details, and Datalist?

Feature Chrome Firefox Safari Edge Global Support
Popover API 114+ 125+ 17+ 114+ ~90%
Details/Summary 12+ 49+ 6+ 79+ ~97%
Details name attribute 120+ 130+ 17.2+ 120+ ~85%
Datalist 20+ 4+ 12.1+ 12+ ~96%

Check caniuse.com for the most current browser support data.

When should I still use JavaScript for UI components?

Use JavaScript when you need:

  • Rich content in dropdown options (images, descriptions)
  • Complex animations beyond CSS transitions
  • Very large datasets requiring virtualization
  • Custom filtering logic or fuzzy search
  • Full combobox patterns with create-new functionality
  • Nested popovers with complex interaction patterns

For most common use cases, native HTML features are sufficient and provide better performance and accessibility.


Sources and References

Official Documentation

Browser Compatibility Tables

Comments

comments powered by Disqus