Structural sibling to The Best CSS Architecture Starts by Deciding What CSS Should Never Do — same distinction, different layer.
Also available in Español
The Problem
A support ticket: a help-center article page looks exactly right. The FAQ section renders, every question is visible, the layout matches the design. Clicking a question does nothing. No error a user would ever see, no broken image, no missing text. The button is there, styled correctly, and pressing it changes nothing at all.
The widget's open and closed states were never represented anywhere the browser could act on without help. They lived entirely in a JavaScript class toggle, attached once, during initialization. Something upstream in that initialization never finished running. The markup that shipped was complete. The mechanism that was supposed to make it interactive never arrived.
Why the Problem Exists
For a long time, the platform didn't provide a native disclosure interaction model. Developers therefore had to construct one themselves: a control, some representation of visibility, and code to coordinate the transition between them. JavaScript became the common place to hold that interaction logic, and the architecture survived after the platform acquired a native alternative.
The trouble is that the reason for that architecture has mostly gone away, and the architecture itself didn't.
The First Principle
The browser doesn't need JavaScript's permission to track whether something is open. Where a native interaction state already exists, the platform owns that state and its user-driven transitions without requiring application JavaScript to implement them. JavaScript can still participate — reading the state, responding to it, or changing it programmatically when the application genuinely needs to — without being the reason the interaction works at all.
That's a narrower claim than it sounds. dialog.showModal() and details.open = true are both JavaScript touching native state directly, and neither one violates it. The distinction isn't whether JavaScript is allowed near an interaction. It's whether JavaScript is the only reason the interaction works.
Demonstrating the Principle
A single disclosure needs nothing from JavaScript at all:
<details>
<summary>What is HTML?</summary>
<p>A markup language for describing the structure of a document.</p>
</details>
Click the summary, and the browser opens it. Click again, and it closes. No class, no listener, no state to initialize. The open attribute is the state, and the browser already knows how to change it.
The harder version of this pattern — the one developers historically recreated with JavaScript — is an exclusive accordion: a group where opening one item closes the others. That coordination used to require tracking every item's state centrally and closing the rest by hand on every click. The platform now owns that too:
<details name="faq">
<summary>What is HTML?</summary>
<p>A markup language for describing the structure of a document.</p>
</details>
<details name="faq">
<summary>What is CSS?</summary>
<p>A language for describing how that structure is presented.</p>
</details>
<details name="faq">
<summary>What is JavaScript?</summary>
<p>A language for adding behavior on top of both.</p>
</details>
Every <details> sharing the same name joins an exclusive group. Opening one closes whichever of the others was open, the same way radio inputs sharing a name already behave. Nothing coordinates this from outside: no shared array of open states, no click handler checking what else is currently expanded. The browser enforces the exclusivity itself.
<dialog> and the Popover API follow the same architectural pattern, at a different scale. The platform owns open, owns the transition between states, and fires a toggle event carrying the same oldState and newState, whether it's a <details> panel or a popover announcing its own change. None of them need JavaScript to exist. All of them accept it when there's an actual reason to be there.
The Pain Point
This shipped as a help-center FAQ: a list of questions, each with a button and a hidden answer, opened and closed entirely through a JavaScript class toggle attached during page initialization. No <details> anywhere. The open and closed state existed only as a CSS class an event listener added and removed.
The initialization responsible for attaching those listeners never completed. The page still rendered completely: every question visible, every answer correctly hidden, nothing missing from a visual pass. The buttons looked exactly like buttons. Pressing one did nothing, because nothing had ever told the browser what pressing it was supposed to do. The interaction had no native fallback to drop back to, because there wasn't a native mechanism underneath it in the first place. Application code had replaced it entirely.
This isn't a claim that JavaScript is unreliable. The defect is architectural: the implementation made successful script initialization a precondition for a state the platform could already represent and transition natively.
Saturday's Notes from the Pass rebuilds the same FAQ on <details name>, removes every line of the original toggle logic, and confirms the accordion behavior holds with the JavaScript file never loading at all.
The Broader Lesson
This isn't an argument against JavaScript owning interaction state. Plenty of state has no native equivalent and never will: which record is selected, whether a request is pending, what a user is currently authenticated as. None of that belongs to HTML, and none of it should.
The question this article keeps returning to is narrower. Before building a mechanism, check whether the platform already has one. Article 4 asked this about focus containment inside a modal. Article 5 asked it about anchoring an overlay to the right element. This is the same question aimed at the simplest possible case, whether something is open or closed, and the simplest case is exactly where the habit of reaching for JavaScript first is hardest to notice, because it's so easy to build that nobody stops to check whether building it was necessary.
What fails is an architecture that made something else — a script, a network request, an initialization order — responsible for a state the platform was already capable of holding by itself.
Top comments (0)