紙吹雪は出た。
字幕は、出なかった。
上映previewまで、あと四十秒。
第9話でイトが見えるようにした「字幕を表示」buttonは、白いcardの上で濃い青に光っている。押せる形も、文字の読みやすさも、もう崩れていない。
イトがbuttonを押す。
青、黄、桃色の紙吹雪が画面いっぱいに舞った。
その下で、アニメの登場人物は口だけを動かしている。音を切った確認端末には、一文字も届かない。
イト
反応はした。でも、ほしい反応じゃない
ユイ
この端末は音を出せません。字幕が開かなければ、台詞が分からないです
ピコ
見えるbuttonと、押した後の仕事は、まだ別々の糸だよ
残り三十六秒。
押された合図は、紙吹雪へ届いていた
操作室の記録には、短い順番が残っていた。
字幕buttonが押される
clickeventが届く登録されたfunctionが走る
紙吹雪を表示する
buttonは壊れていない。
clickも消えていない。
合図を受け取った先が、字幕ではなく飾りだった。
現在のcodeは、こうなっている。
const button = document.querySelector("#caption-toggle");
button.addEventListener("click", launchSparkles);
addEventListener()は、特定の種類のeventを観察するlistenerを登録する。eventがtargetへ届くと、登録したcallbackが呼ばれる。
DOM Standardは、event、EventTarget、event listenerと、documentを構成するnode treeのmodelを定めている。
「JavaScript」という言葉だけで、buttonやdocumentの仕組みまですべて定義しているわけではない。
ECMAScript 2026 Language Specificationが言語の側を定め、browserというhost environmentがDOMなどのWeb APIを渡す。その組み合わせで、codeがpageの部品へ触れられる。
マコトが、処理の札を二枚に分けた。
合図を受け取る
受け取った後に何をする
マコト
今回は一枚目まで成功している。直す場所は、押せる見た目ではなく、二枚目の仕事だ
イト
光ったから完成だと思った。画面の目的まで見てなかった
残り二十九秒。
画面は、部品と状態の集まりになっている
字幕buttonの隣には、最初から一行分の字幕があった。
<button id="caption-toggle" aria-pressed="false">
字幕を表示
</button>
<p id="caption-line" hidden>
扉の向こうで、最後の一席が光った。
</p>
字幕は、まだserverへ取りに行くdataではない。
すでにpageの中にあり、hiddenの状態なので表示されていない。
browserはHTMLから、document、element、textなどがつながるtreeを作る。これを入口から見たものがDOMだ。
JavaScriptはdocument.querySelector()などを使い、対象のnodeへたどり着ける。そしてpropertyやtextを変えれば、browserは現在の画面へその状態を反映できる。
今回変えるものは三つだけだ。
- 字幕の
hidden - buttonの
aria-pressed - buttonに表示する文字
「字幕を表示」と書いてあるのに字幕が開いたままなら、次に押したときの動作と文字が食い違う。
字幕だけを見せれば終わりではない。操作前、操作後、もう一度押した後まで、同じ状態を指さす必要がある。
三つの修正札
ピコが、残り時間で試せる三枚の札を出した。
- もっと光らせる
- 字幕を開いたままにする
- 紙吹雪の仕事を字幕へつなぎ替える
一枚目なら、CSSの発光を強くできる。押した手応えは増えるが、台詞は読めない。
二枚目なら、hiddenを外して字幕を常に見せられる。音を出せない端末でも読めるが、「字幕を表示」というbuttonは嘘のまま残る。
三枚目なら、紙吹雪を消し、clickを字幕の状態変更へつなげられる。ただし本番から目立つ演出が一つ消える。pointerだけでなくkeyboardでも、開く・閉じるの往復を確かめる時間が必要だ。
画面の上では、紙吹雪がまだゆっくり落ちていた。
イトは、この演出を気に入っていた。
上映の始まりを、いちばん派手に見せるために自分で選んだ色だった。
残り二十二秒。
ピコ
どの札を使う?
ユイ
私は答えを決めません。音のない端末で何を届けるかは、作る人の選択です
イトは、紙吹雪の札を裏返した。
「これは外す」
指で飾りの糸を抜き、字幕のnodeへ結び直した。
イトが、押した後の仕事を書き換える
const button = document.querySelector("#caption-toggle");
const caption = document.querySelector("#caption-line");
button.addEventListener("click", () => {
const willOpen = caption.hidden;
caption.hidden = !willOpen;
button.setAttribute("aria-pressed", String(willOpen));
button.textContent = willOpen ? "字幕を隠す" : "字幕を表示";
});
一行ずつ、イトが確かめる。
buttonがeventのtargetになる。
clickが届く。
listenerに登録したfunctionが走る。
functionは、字幕がこれから開くのかを読む。
そして字幕、pressed state、buttonの文字を、同じ結果へそろえる。
event
listener
callback
DOMの状態変更
画面の反映
この順番が、ようやく一本につながった。
pointerで一往復、keyboardでも一往復
残り十三秒。
イトはpointerでbuttonを押す。
紙吹雪は出ない。
代わりに、画面の下へ一行が現れた。
扉の向こうで、最後の一席が光った。
buttonの文字は「字幕を隠す」へ変わり、もう一度押すと字幕は閉じた。
次にpointerから手を離し、keyboardでbuttonへ移動する。
キーでbuttonを作動させる。
字幕が開く。
もう一度作動させる。
字幕が閉じる。
HTML Standardのactivation behaviorでは、activation behaviorを持つelementをkeyboardやvoiceなどで手動作動させた場合も、既定のactionとしてそのelementへclick eventを発火する流れが定められている。実際の操作方法はbrowserや支援技術で異なるため、mouseだけの確認で終えない。
イト
飾りは消えた。でも、押した人がほしかった変化は残った
マコト
動いたかではなく、操作と結果が一致したかを見たんだね
残り四秒。

紙吹雪のない本番
previewが始まった。
音を切った端末で、イトが「字幕を表示」を押す。
登場人物の口が動く。
その下へ、同じ台詞が一行ずつ現れる。
紙吹雪は一枚も落ちない。
画面の隅で、イトが外した演出札だけが伏せられていた。
それでも、buttonの文字と字幕の状態は、最後まで一度も食い違わなかった。
JavaScriptは、画面を派手にするためだけの道具ではない。
利用者の操作をeventとして受け取り、必要な処理を選び、pageの現在状態を変える。その結果が見える形で返ってきて、初めて一つの反応になる。
ただし、今回開いた字幕は、最初からpageの中にあった。
previewの終了直前、隣の端末で予約札が一枚動いた。
イトの画面には、まだ青い数字が残っている。
最終話上映 残り1席
別の端末では、最後の席がすでに選ばれていた。
字幕buttonは現在の状態へ変わる。
残席counterは、古い1から動かない。
イトは紙吹雪の札ではなく、二つの数字を見比べた。
「pageの外で変わった今のdataは、どうやって取りに行くんだろう」
次回:あとから届くデータはどこから来る?
buttonを押した合図から、page内の字幕を開く道はつながった。
けれど、別の場所で変わった残席は、手元のDOMを切り替えるだけでは分からない。
どこへrequestを送り、どんなresponseを受け取り、古い1を現在の0へ直すのか。
次回、ピコルート第11話。
イトは、最後の一席を手放す。
画面のむこう側をもっと知る
物語で見たevent、listener、DOM、画面更新を用語として整理するなら、JavaScriptとはで図と一緒に確かめられます。

