Interactive & Scripting Elements - HTML reference 2026 vdsology
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 Esccloserequest— Closes on Esc only. Default forshowModal().none— Only closes via JavaScriptclose(),method="dialog"form, orcommand="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-labelledbypointing to heading inside - Use
autofocuson safest button - Never suppress Esc key without good reason
tabindexmust 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.contentis aDocumentFragment- 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