style.display = '' は「元に戻す」ではない
表示と非表示を el.style.display = 'none' と el.style.display = '' のペアで書くのは定石だ。
ほとんどの場合は正しく動く。動かないのは、その要素の display が inline 属性にしか書かれていないときだ。
= '' は「元に戻す」ではなく、inline style から display 宣言を削除する操作だ。
削除された結果、その要素の display は CSS のカスケードで決まる。
どこにも規則が無ければ、div の既定値である block に落ちる。
<div id="bar" style="display:flex">…</div>
この要素を一度でも = 'none' → = '' すると、flex は永久に失われる。
以後この行は横並びにならない。
「JS が触る値をクラスに入れない」方針と噛み合うと最悪になる
ある社内 Web ツールのユーティリティ CSS は、
「JS が inline へ書くプロパティは 1 つも入れない」という方針で書かれていた。.u-wrap は flex-wrap だけ、.u-gap-8 は gap だけを持ち、display は持たない。
この方針そのものは正しい。クラス側に display を入れると、
JS が書く inline style と競合して !important の応酬になる。
ところがこの方針は、= '' に対するフォールバック先を消すという副作用を持っていた。
削除された flex を拾ってくれる規則がどこにも無いので、確実に block へ落ちる。
実際、ビューを切り替えた瞬間にツールバーが display: block に化け、
横並びだったはずの 2 つの入力欄(受領元と対象月)が縦に積み上がっていた。
なぜスクリーンショットで気づけないか
狭い幅では、縦積みが自然に見える。
レスポンシブなツールバーは、そもそも幅が足りなければ折り返す設計になっている。
だから縦に並んだ画面を見ても「そういうレイアウトだ」としか思わない。
色が消えたりテキストが欠けたりする欠陥と違って、目視レビューが機能しない類の壊れ方だ。
見えない以上、読むしかない。
const before = getComputedStyle(bar).display; // 'flex'
bar.style.display = ''; // 旧コードと同じ書き込み
const after = getComputedStyle(bar).display; // 'block' ← これが証拠
計算済みスタイルを実測すれば 2 行で決着がつく。
スクリーンショットの比較では、いつまでも決着しない。
直し方は「親を触らない」
3 通りある。上ほど安全だ。
- display を inline に持つ要素の display を、JS から触らない。
出し入れするのはその中の子要素にする(親コンテナは常設・子だけトグル)
- どうしても親をトグルするなら、
= ''ではなく= 'flex'を明示的に書き戻す - display をクラス側に持たせ、JS は
classList.toggle('is-hidden')だけを触る
(ただしこれは前述の方針転換になるので、既存の inline 書き込みを全部数えてからにする)
採用したのは 1 番だ。親のツールバーは常に flex のまま置きっぱなしにして、
ビューによって出し入れしたい入力欄だけを子として切り替える。
JS が display を書く要素と、display を inline に持つ要素を、重ねないという規律に落ちる。
同じ穴は display 以外にもある
= '' が「inline の宣言を削除する」である以上、inline にしか値が無いプロパティは全部同型だ。
| プロパティ | = '' で落ちる先 |
|---|---|
display: flex | block(要素の既定値) |
visibility: hidden | visible |
position: absolute | static(絶対配置が解けて文書の流れに戻る) |
position は特に厄介で、レイアウトが崩れるのは離れた場所になる。
まとめ
style.X = ''は「元に戻す」ではなく「inline の宣言を削除する」- inline にしか無い値は、削除された瞬間に既定値へ落ちる
- ユーティリティ CSS に display を入れない方針は正しいが、フォールバック先が無くなる
- 出し入れは子要素で行い、親コンテナは常設にする
- 検証はスクリーンショットではなく
getComputedStyleの実測で行う