Interactive & Scripting Elements - HTML reference 2026 vdsology

vdsology html reference 2026 chapter 9

Chapter 9 — Interactive & Scripting Elements

This chapter covers elements that create native interactive behaviour — disclosure widgets, modal dialogs, floating popovers — and the templating and Web Component infrastructure of <template> and <slot>.


9.1 — <details> — Disclosure Widget

Implicit ARIA role: group

Attributes:

Attribute Values Purpose
open Boolean Expanded state
name String Exclusive accordion group (cross-browser August 2024)

When multiple <details> share the same name, they form an exclusive group — opening one closes all others with the same name.

<!-- Independent disclosures -->
<details>
  <summary>What is CSS Grid?</summary>
  <p>CSS Grid is a two-dimensional layout system…</p>
</details>

<!-- Exclusive accordion: opening one closes others -->
<details name="faq">
  <summary>What is your refund policy?</summary>
  <p>Full refunds within 30 days.</p>
</details>
<details name="faq">
  <summary>How long does shipping take?</summary>
  <p>3–5 business days.</p>
</details>
<details name="faq" open>
  <summary>Do you offer free shipping?</summary>
  <p>Yes, on orders over £50.</p>
</details>
/* Styling */
details summary::marker { content: '▶ '; }
details[open] summary::marker { content: '▼ '; }

/* Animation */
details > :not(summary) {
  animation: slide-down 0.3s ease-out;
}
@keyframes slide-down {
  from { opacity: 0; transform: translateY(-0.5rem); }
  to   { opacity: 1; transform: translateY(0); }
}
// Toggle event
details.addEventListener('toggle', (event) => {
  console.log(event.newState); // 'open' or 'closed'
});

Nesting rules: First child should be <summary>. Then any flow content. Can be nested inside itself.

Do NOT use for: Tab interfaces, context menus, tooltips, footnotes.


9.2 — <summary> — Disclosure Summary

The always-visible clickable heading of a <details> element.

Implicit ARIA role: button

No element-specific attributes.

Content model: Phrasing content OR one heading element (<h1><h6>).

Must be the first child of <details>.

<details>
  <summary>Getting started</summary>
  <p>Content here…</p>
</details>

<!-- With heading — valid -->
<details>
  <summary><h2>Advanced Configuration</h2></summary>
  <p>Settings for power users…</p>
</details>

<!-- Rich summary -->
<details>
  <summary>
    <span aria-hidden="true">🔧</span>
    Settings
    <span class="badge">3 warnings</span>
  </summary>
</details>
/* Remove default triangle */
summary { list-style: none; cursor: pointer; }
summary::-webkit-details-marker { display: none; }

/* Custom indicator */
summary::before { content: '+ '; font-weight: bold; }
details[open] summary::before { content: '− '; }

9.3 — <dialog> — Dialog Box

Implicit ARIA role: dialog

Attributes:

Attribute Values Purpose
open Boolean Dialog is visible (prefer using showModal()/show())
closedby any, closerequest, none Light dismiss control (Chrome 134+)

closedby values:

  • any — Light dismiss: closes on outside click OR Esc
  • closerequest — Closes on Esc only. Default for showModal().
  • none — Only closes via JavaScript close(), method="dialog" form, or command="close"

Modal vs Non-Modal:

Modal showModal() Non-modal show()
Top layer
::backdrop
Focus trapping
Background inert
Closes on Esc ✓ (default)
<!-- Opened by Invoker Commands (no JavaScript) -->
<button type="button" command="show-modal" commandfor="settings-modal">
  ⚙ Settings
</button>

<dialog id="settings-modal" closedby="closerequest"
        aria-labelledby="settings-title">
  <header>
    <h2 id="settings-title">Settings</h2>
    <button type="button" command="close" commandfor="settings-modal"
            aria-label="Close settings">✕</button>
  </header>

  <form method="dialog">
    <fieldset>
      <legend>Theme</legend>
      <label><input type="radio" name="theme" value="light"> Light</label>
      <label><input type="radio" name="theme" value="dark"> Dark</label>
      <label><input type="radio" name="theme" value="system" checked> System</label>
    </fieldset>
    <div>
      <button type="submit" value="save" autofocus>Save changes</button>
      <button type="submit" value="cancel" formnovalidate>Cancel</button>
    </div>
  </form>
</dialog>
const dialog = document.getElementById('my-dialog');
dialog.showModal();          // Modal (focus trapped)
dialog.show();               // Non-modal
dialog.close();              // Close
dialog.close('confirmed');   // Close with return value

dialog.addEventListener('close', () => {
  console.log(dialog.returnValue); // value from submit button
});
dialog.addEventListener('cancel', (e) => {
  e.preventDefault(); // prevent Esc closing
});
dialog {
  border: none;
  border-radius: 0.5rem;
  box-shadow: 0 20px 60px rgb(0 0 0 / 0.3);
  padding: 2rem;
  max-width: min(90vw, 480px);
}

dialog::backdrop {
  background: rgb(0 0 0 / 0.5);
  backdrop-filter: blur(4px);
}

/* Entry animation using @starting-style */
@starting-style {
  dialog[open] {
    opacity: 0;
    transform: scale(0.95) translateY(-1rem);
  }
}
dialog[open] {
  opacity: 1;
  transform: scale(1) translateY(0);
  transition: opacity 0.2s ease, transform 0.2s ease,
              display 0.2s ease allow-discrete,
              overlay 0.2s ease allow-discrete;
}

Accessibility rules:

  • Label with aria-labelledby pointing to heading inside
  • Use autofocus on safest button
  • Never suppress Esc key without good reason
  • tabindex must NOT be set on <dialog> itself

9.4 — The Complete Popover API

popover attribute

Any element can be a popover. Three values:

popover="auto" — Light dismissible. One auto open at a time. Nested auto popovers coexist. popover="hint" (cross-browser January 2025): Coexists with auto popovers. For tooltips. popover="manual": No auto-close. Multiple can be open simultaneously.

popovertarget

ID of the popover to control. On <button> or <input type="button/reset">.

popovertargetaction

toggle (default), show, or hide.

<button popovertarget="nav-menu">Menu ▼</button>
<nav id="nav-menu" popover role="menu" aria-label="Navigation">
  <a href="/" role="menuitem">Home</a>
  <a href="/about" role="menuitem">About</a>
</nav>

<!-- Hint tooltip alongside open menu -->
<div id="main-menu" popover>
  <button interestfor="new-tip">What's New 🔥</button>
</div>
<div id="new-tip" popover="hint" role="tooltip">
  See our 3 new features!
</div>

<!-- Manual: persistent notification -->
<div id="toast" popover="manual" role="status" aria-live="polite">
  Saved!
  <button popovertarget="toast" popovertargetaction="hide">✕</button>
</div>
const popover = document.getElementById('my-popover');
popover.showPopover();
popover.hidePopover();
popover.togglePopover();
popover.togglePopover({ force: true }); // force show

popover.addEventListener('toggle', (e) => {
  console.log(e.newState); // 'open' or 'closed'
});
popover.addEventListener('beforetoggle', (e) => {
  e.preventDefault(); // cancel the toggle
});

Focus model: Popovers do NOT auto-focus or trap focus (unlike modal dialogs). Use autofocus on an element inside if needed. If you need focus trapping, use <dialog> instead.

Dialog vs Popover:

Concern Popover Modal Dialog
Blocks background
Focus trapping
Light dismiss default
Use for Menus, tooltips, toasts Confirmations, auth, decisions

9.5 — <template> — HTML Template

Holds inert HTML — parsed but not rendered, executed, or accessible until activated.

Attributes:

Attribute Values Purpose
shadowrootmode open, closed Declarative Shadow DOM (Chrome/Safari/Firefox)
shadowrootdelegatesfocus Boolean Redirect focus to first focusable in shadow root
shadowrootclonable Boolean Shadow root is cloned when host is cloned
shadowrootserializable Boolean Shadow root serialised by getHTML()
<!-- Standard template: cloned by JavaScript -->
<template id="card-tpl">
  <article class="card">
    <img class="card-img" alt="">
    <h2 class="card-title"></h2>
    <p class="card-desc"></p>
  </article>
</template>

<script>
const tpl = document.getElementById('card-tpl');
const data = [{ title: 'Widget', desc: 'A great widget', img: '/widget.jpg' }];
data.forEach(item => {
  const clone = tpl.content.cloneNode(true);
  clone.querySelector('.card-img').src = item.img;
  clone.querySelector('.card-img').alt = item.title;
  clone.querySelector('.card-title').textContent = item.title;
  clone.querySelector('.card-desc').textContent = item.desc;
  document.getElementById('grid').append(clone);
});
</script>

<!-- Declarative Shadow DOM: no JavaScript needed -->
<my-card>
  <template shadowrootmode="open">
    <style>
      :host { display: block; border: 1px solid #e5e7eb; border-radius: 0.5rem; }
      ::slotted(img) { width: 100%; }
    </style>
    <slot name="image"></slot>
    <h2><slot name="title">Untitled</slot></h2>
    <slot></slot>
  </template>

  <!-- Light DOM fills slots -->
  <img slot="image" src="product.jpg" alt="Product">
  <span slot="title">Widget Pro</span>
  <p>The most advanced widget.</p>
</my-card>

Key behaviours:

  • template.content is a DocumentFragment
  • Scripts don’t execute, images don’t load until cloned
  • Can contain context-dependent elements (<td>, <li>) without parents
  • With shadowrootmode: browser removes <template> and attaches shadow root to parent

9.6 — <slot> — Shadow DOM Slot

A placeholder inside shadow DOM where light DOM content is projected.

Attribute:

Attribute Purpose
name Named slot — receives children with matching slot="name". Unnamed = default slot.
<template shadowrootmode="open">
  <article>
    <header>
      <h1><slot name="title">Untitled</slot></h1>
      <p class="byline">By <slot name="author">Anonymous</slot></p>
    </header>
    <div class="body">
      <slot></slot>  <!-- default slot: receives unslotted children -->
    </div>
    <footer>
      <slot name="tags">
        <p>No tags</p>  <!-- fallback: shown if slot is empty -->
      </slot>
    </footer>
  </article>
</template>

<!-- Light DOM: fills slots -->
<span slot="title">Understanding CSS Grid in 2026</span>
<span slot="author">Alice Smith</span>
<!-- Goes to default slot: -->
<p>CSS Grid has transformed web layout…</p>
<ul slot="tags"><li>CSS</li><li>Grid</li></ul>

CSS for slots:

/* Style slotted content (direct children only) */
::slotted(p) { line-height: 1.7; }
::slotted(*) { box-sizing: border-box; }

/* CSS custom properties pass through shadow boundary */
:host { --accent: var(--page-accent, #2563eb); }

::part() — styling shadow internals:

<template shadowrootmode="open">
  <button part="trigger">Click</button>
</template>
my-component::part(trigger) { background: #2563eb; color: white; }

9.7 — Browser Support Summary

Feature Browser support
<details> / <summary> All browsers — always
<details name> exclusive accordion Cross-browser August 2024
<dialog> Cross-browser since March 2022
closedby on <dialog> Chrome 134+ (in progress)
Popover API (auto/manual) Cross-browser January 2025
popover="hint" Cross-browser January 2025
command/commandfor Cross-browser December 2025
interestfor Chrome Canary — experimental only
<template> (standard) All browsers — always
Declarative Shadow DOM (shadowrootmode) Chrome 111+, Safari 16.4+, Firefox 123+
shadowrootclonable, shadowrootserializable Newer Chrome/Safari/Firefox


Tags: #html #vdsology #webdevelopment #vdsologyhtml #html5 #htmlreference #htmlreference2026


Write a comment
No comments yet.