A living exhibition of the modern web platform
HTML can do that?!
Popovers, modals, accordions, autocomplete, lazy loading, validation — things you were told need a JavaScript library. They don’t. Below are 13 live exhibits, each one rendered with the actual HTML feature it celebrates. Touch everything. That’s the point.
👋 Hi from the top layer.
This greeting card is a popover. No z-index wars, no event listeners,
no document.querySelector. It opened because a button said
popovertarget="hero-popover" — and it closes when you click anywhere else or press
Esc. That trick is exhibit 01, and there are twelve more where it came from.
- 13
- live exhibits
- 0 B
- JavaScript per demo
- 100%
- real, clickable HTML
Exhibit 01
popover
Tooltips, menus, toasts, teach-bubbles — anything that floats above the page.
One attribute puts an element in the top layer, gives you
light-dismiss (click outside or Esc) and wires the trigger button.
No positioning library, no focus trap code, no z-index: 99999.
Full support matrix
| Desktop | Chrome 114 · Edge 114 · Firefox 125 · Safari 17 · Opera 100 |
|---|---|
| Mobile | Chrome Android 114 · Firefox Android 125 · Safari iOS 17 · Samsung Internet 23 |
| Fallback | Content stays in the page flow — hidden with a tiny [popover]{display:none} guard until opened. |
Then click anywhere outside it, or press Esc. That’s light-dismiss, free of charge.
Admit one
The Top Layer
I render above everything on this page — no z-index was harmed.
My opener button has never met a addEventListener.
<button popovertarget="ticket">Summon the popover</button>
<div id="ticket" popover>
<p>I live in the top layer. Click outside or
press Esc — light-dismiss is built in.</p>
<button popovertarget="ticket"
popovertargetaction="hide">Close</button>
</div>
Exhibit 02
<dialog>
A bona-fide modal: the page behind it goes inert, focus stays inside,
Esc closes it, and ::backdrop dims the world.
Closed with a plain <form method="dialog"> — the chosen button’s
value even becomes the dialog’s returnValue.
Full support matrix
| Desktop | Chrome 37 · Edge 79 · Firefox 98 · Safari 15.4 · Opera 24 |
|---|---|
| Mobile | Chrome Android 37 · Firefox Android 98 · Safari iOS 15.4 · Samsung Internet 3 |
| Opened here by | An invoker button (command="show-modal") — see exhibit 04. Older browsers can call .showModal() in one line of JS. |
While it’s open, try clicking this text or tabbing away. You can’t. The page is inert.
<button command="show-modal" commandfor="modal">
Open the modal
</button>
<dialog id="modal">
<form method="dialog">
<p>Focus trapped. Page inert. Esc closes.</p>
<button value="yes">Yes</button>
<button value="no">No</button>
</form>
</dialog>
Exhibit 03
Grouped <details>
Give several <details> elements the same name and the browser
turns them into an exclusive accordion — open one, its siblings snap shut.
Keyboard accessible, screen-reader friendly, and the state machine is not your problem.
Full support matrix
| Desktop | Chrome 120 · Edge 120 · Firefox 130 · Safari 17.2 · Opera 106 |
|---|---|
| Mobile | Chrome Android 120 · Firefox Android 130 · Safari iOS 17.2 · Samsung Internet 25 |
| Graceful degradation | Without name support, each panel still opens and closes — just independently. |
🚪 Room one — the lobby
Welcome in. Notice I started open, because someone put the boolean
open attribute on me. No script measured my height or toggled a class.
📦 Room two — the archive
The moment you opened me, Room one closed itself. That’s the shared
name="rooms" doing radio-button logic on disclosure widgets.
🎁 Room three — the gift shop
Try me with a keyboard: Tab to a summary, Enter or Space to toggle. Accessible by default because the semantics are real.
name away<details name="rooms" open>
<summary>Room one</summary>
<p>Open the next room and I close on my own.</p>
</details>
<details name="rooms">
<summary>Room two</summary>
<p>Exclusive. Automatic. Native.</p>
</details>
Exhibit 04
command / commandfor
Invoker buttons: a button declares what it does and which element it does it to.
Three separate buttons remote-control one popover below — show, hide, toggle — and the
browser wires the events, the aria-expanded state, everything.
Full support matrix
| Desktop | Chrome 135 · Edge 135 · Firefox 144 · Safari 26.2 · Opera 120 |
|---|---|
| Mobile | Chrome Android 135 · Firefox Android 144 · Safari iOS 26.2 · Samsung Internet 29 |
| Today’s verbs | show-modal, close, request-close, toggle-popover, show-popover, hide-popover. Media and value commands are on the way. |
Three buttons, one target, zero listeners. The toast also light-dismisses.
📡 Invoked. No JavaScript touched this toast — three different buttons did.
<button command="show-popover" commandfor="toast">Show</button>
<button command="hide-popover" commandfor="toast">Hide</button>
<button command="toggle-popover" commandfor="toast">Toggle</button>
<div id="toast" popover>Invoked. No JavaScript.</div>
Exhibit 05
Native pickers
Color wells, sliders with tick marks, calendar popups, time spinners —
the browser ships a whole control room. Each one is keyboard- and touch-ready,
localised automatically, and themed with a single accent-color.
Per-type support matrix
| color | Chrome 20 · Edge 14 · Firefox 29 · Safari 12.1 |
|---|---|
| range | Chrome 4 · Edge 12 · Firefox 23 · Safari 3.1 |
| date / time | Chrome 20 · Edge 12 · Firefox 57 · Safari 14.1 |
| Caveat | Styling depth varies by browser — treat the UA chrome as a feature, not a bug. |
Twiddle every control — they’re all real. (A furniture script echoes your picks here.)
<input type="color" value="#ff3d81">
<input type="range" min="0" max="11" list="ticks">
<datalist id="ticks">
<option value="0"><option value="5"><option value="11">
</datalist>
<input type="date">
<input type="time" value="19:30">
Exhibit 06
<datalist>
Autocomplete without a combobox library. One list attribute joins an input to a
set of <option>s, and the browser filters as you type — fuzzy, instant,
arrow-key navigable.
Full support matrix
| Desktop | Chrome 20 · Edge 12 · Firefox 4 · Safari 12.1 · Opera 9.5 |
|---|---|
| Mobile | Chrome Android 33 · Firefox Android 79 · Safari iOS 12.2 · Samsung Internet 2 |
| Caveats | The dropdown’s look is browser-controlled, and screen-reader behaviour still varies — see Adrian Roselli’s Under-Engineered Comboboxen. |
That dropdown you just saw? It’s not a component. It’s an element.
<input list="elements" placeholder="Start typing…">
<datalist id="elements">
<option value="dialog">
<option value="details">
<option value="datalist">
<!-- … -->
</datalist>
Exhibit 07
Constraint validation
required, type="email", pattern, minlength,
min/max — the browser checks everything on submit, shows native
error bubbles, and focuses the first offender. CSS hooks like
:user-valid / :user-invalid style the aftermath.
Full support matrix
| Attributes | required · pattern · minlength/maxlength · min/max/step · typed inputs — universal for a decade. |
|---|---|
:user-valid / :user-invalid | Chrome 119 · Edge 119 · Firefox 88 · Safari 16.5 |
| Zero-JS limit | The success message below uses one furniture script to stay on-page; the validation itself is 100% native. |
✅ Valid on the first try — the browser checked every field. No validation library attended.
<input required minlength="3" pattern="[a-z0-9-]+">
<input type="email" required>
<input type="number" min="0" max="99">
/* and the styling hooks */
input:user-valid { border-color: green; }
input:user-invalid { border-color: red; }
Exhibit 08
<meter> & <progress>
Gauges and bars with semantics. A <meter> knows its
low/high/optimum zones and colors itself accordingly;
a <progress> without a value becomes an indeterminate spinner-bar.
Full support matrix
| meter | Chrome 6 · Edge 12 · Firefox 16 · Safari 5.2 · Opera 11 |
|---|---|
| progress | Chrome 8 · Edge 12 · Firefox 6 · Safari 6 · Opera 11 |
| Note | Values are declarative — update them from real data (or a furniture script) when they change. |
JavaScript powering this page’s exhibits
0 KB — optimum zone, green
Delight detected in visitors
92 / 100 — above optimum, still green
Battery of a demo laptop at a conf talk
14 / 100 — danger zone, red
Reading this sentence
Done — determinate
Waiting for a build to finish
Forever — indeterminate
<meter min="0" max="100" low="30" high="80"
optimum="90" value="92">92%</meter>
<progress max="100" value="64">64%</progress>
<!-- no value → indeterminate -->
<progress>loading…</progress>
Exhibit 09
<audio controls>
Play, pause, seek, volume, download — an entire media player materialises from one boolean attribute. Below: an 8-bit chiptune synthesised for this exhibition, playing through the browser’s own deck.
Full support matrix
| audio | Every shipping browser since ~2010. <video controls> same story. |
|---|---|
| Useful attributes | controls · loop · muted · preload · poster (video) |
| This track | “Stacked Elements” — square waves generated in Python, no samples, 210 KB WAV. |
<audio controls preload="none" src="chiptune.wav">
Your browser doesn’t do audio? Impressively retro.
</audio>
Exhibit 10
contenteditable
The original rich-text editor — and it’s an attribute, not a framework. Any element becomes writable: click the card below and type. Spellcheck comes along for free.
Full support matrix
| Desktop & mobile | Literally every browser you’ll ever meet. |
|---|---|
| Companions | spellcheck="true" · enterkeyhint · inputmode shape the on-screen keyboard on mobile. |
| Caveat | Serialization is famously quirky — for serious editors, libraries still earn their keep. For an inline “edit me”? Perfect. |
Dear exhibition, I always thought I needed JavaScript to
Finish that sentence. Then watch the squiggly spellcheck underline — also native.
<p contenteditable="true" spellcheck="true">
Dear exhibition, I always thought…
</p>
Exhibit 11
loading="lazy"
Images that fetch only when they approach the viewport. No IntersectionObserver, no placeholder dance you wrote yourself. Scroll the gallery corridor below and watch the badges flip from deferred to loaded as you go.
Full support matrix
| Desktop | Chrome 77 · Edge 79 · Firefox 75 · Safari 15.4 · Opera 64 |
|---|---|
| Mobile | Chrome Android 77 · Firefox Android 79 · Safari iOS 15.4 · Samsung Internet 12 |
| The badges | The “deferred / loaded” readout is page furniture (~10 lines of JS) that only reports what the browser did natively. Watch your network tab to verify. |
Checking the gallery…
<img src="poster-01.svg" loading="lazy" decoding="async"
width="800" height="1000" alt="…">
<!-- width & height reserve the space: no layout shift -->
Exhibit 12
hidden="until-found"
Content that’s hidden from view but findable. Hit a fragment link or use find-in-page, and the browser reveals the section and scrolls you right to it — the search-friendliest accordion state ever specced.
Full support matrix
| Desktop | Chrome 102 · Edge 102 · Firefox 148 · Safari 26.2 · Opera 88 |
|---|---|
| Mobile | Chrome Android 102 · Firefox Android 148 · Safari iOS 26.2 · Samsung Internet 19 |
| Two ways in | Fragment navigation (#treasure) or find-in-page — both reveal the element automatically. |
Somewhere in this stage hides a treasure. It is invisible, it takes up no space… but the browser knows where it is.
🗺 Follow the mapSceptic? Press ⌘F / Ctrl+F and search for “yahaha” instead.
Yahaha! You found me!
I was hidden="until-found" — removed from the layout entirely, yet the browser
still indexed me, revealed me, and scrolled here. Searchable collapsible content, no JS accordion state.
<a href="#treasure">Follow the map</a>
<div id="treasure" hidden="until-found">
<p>Yahaha! You found me!</p>
</div>
<!-- ⌘F works too: the browser auto-reveals matches -->
Exhibit 13
scroll-behavior: smooth
You’ve been riding this exhibit the whole time. Every jump from the navigation glided here on one CSS declaration — plus anchor links that need no router, no scroll-spy library to move. (The active-item highlighting in the sidebar is furniture JS; the gliding is not.)
Full support matrix
| Desktop | Chrome 61 · Edge 79 · Firefox 36 · Safari 15.4 · Opera 48 |
|---|---|
| Respect motion | Wrap it in @media (prefers-reduced-motion: no-preference) — as this page does. |
Each jump is a plain <a href="#…">. The glide is one line of CSS.
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
}
<a href="#treasure">Take me there</a>
The platform is the library.
Thirteen exhibits, every one interactive, every one running on markup alone. JavaScript is wonderful — but the best byte is the one you never ship.
Tour it again ↺ The original list that inspired this exhibition ↗
Honest footnote: this page ships ~4 KB of furniture JavaScript — scroll-spy highlighting, copy buttons, the lazy-load badges and two demo readouts. Each exhibit runs on 0 bytes. View source and check.