フォームの真実:`:read-only` と `:read-write` が描くモダンWebアプリケーションの境界線
こんにちは。日夜、CSSOMの構築コストやブラウザの再描画(Repaint/Reflow)の最適化に心を奪われているフロントエンド・アーキテクトの私だ。
モダンなWebアプリケーション開発において、UIの状態管理は複雑さを増す一方だ。ReactやVueといったフレームワークのステートとDOMの状態が同期する中で、私たちはしばしば「この入力欄、今は編集できるんだっけ、それともリードオンリーだっけ?」というスタイリングの分岐に直面する。
クラス名で `.is-disabled` や `.is-readonly` を量産し、JavaScript側で状態変化のたびにクラスを付け替える……。そんな不毛なDOM操作に疲れていないだろうか?
ブラウザは最初から、その要素が「読み取り専用」なのか「編集可能」なのかを完璧に知っている。ならば、そのネイティブな状態をCSSのセレクタで直接捉えにいけばいい。今回は、`:read-only` と `:read-write` という、過小評価されがちだが極めて強力な擬似クラスを用いて、堅牢でメンテナンス性の高いスタイルアーキテクチャを構築する方法を深掘りしていこう。
—
1. 基礎的メカニズム:ブラウザは何を「読み取り専用」とみなすのか?
まず、大前提としてこれらの擬似クラスが何をターゲットにしているのかを正確に把握する必要がある。なんとなく「`readonly` 属性がついた input」だと思っているなら、仕様の深淵を見落としている。
`:read-write` は、ユーザーが編集可能なすべての要素にマッチする。
- ``(`readonly` や `disabled` がついていないもの)
- `
- `contenteditable` 属性が明示的あるいは暗黙的に有効な要素
- その他、ユーザー入力を受け付ける一部のフォーム要素
対して `:read-only` は、その対極だ。
- `readonly` 属性が付与された `` や `
- `disabled` が付与された要素(※注意:ブラウザの実装や仕様の変遷において `disabled` は独自のスタイルを持つが、広義の読み取り専用としても機能する)
- デフォルトで編集不可な通常のテキスト要素(`
` や `
` など、特に `contenteditable` がないもの)
ここで重要なのは、「JavaScriptで状態を監視してクラスをトグルするコストを、CSSのネイティブ評価に置き換える」というアーキテクチャ上の思想だ。ブラウザのレンダリングエンジンは、属性の変化やフォーカスの状態を内部のC++層で高速にトラッキングしている。JavaScriptのイベントループを介さず、CSSエンジンに直接判定を委ねる方が、メモリ効率の面でも実行速度の面でも圧倒的に有利なのだ。
—
2. アーキテクチャの実装:実戦的なフォームスタイリング
では、実際のコードを見てみよう。単に色を変えるだけではない。堅牢なデザインシステムに組み込むための、洗練されたCSS設計のサンプルだ。
/ ==========================================
フォーム要素のベースアーキテクチャ
========================================== /.form-control {
/ 共通のレイアウトとトランジション設定 /
font-size: 1rem;
padding: 0.75rem 1rem;
border: 1px solid var(–color-border-base);
border-radius: var(–radius-md);
background-color: var(–color-bg-surface);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
width: 100%;
}/ 編集可能な状態 (:read-write)
ユーザーがフォーカスした際のインタラクティブな挙動を定義 /
.form-control:read-write:focus {
outline: none;
border-color: var(–color-primary);
box-shadow: 0 0 0 3px var(–color-primary-alpha);
}/ 読み取り専用の状態 (:read-only)
ユーザーに「ここは触れない(あるいは触る必要がない)」ことを視覚的に伝える /
.form-control:read-only {
background-color: var(–color-bg-muted);
border-color: var(–color-border-subtle);
color: var(–color-text-muted);
cursor: default;
/ 読み取り専用時は、フォーカス時の不必要なリングを描画させないことでUXを向上 /
box-shadow: none;
}このアプローチの美しいところは、HTML側で `` と書くだけで、CSS側が自動的に適切なスタイルを適用してくれる点だ。開発者がうっかり `.is-readonly` クラスを付け忘れるというヒューマンエラーを、コンパイラやフレームワークに頼らず、CSSの仕様そのもので完全にハイドレート(封殺)できる。
—
3. 真骨頂:`contenteditable` との融合によるリッチエディタの制御
さて、ここからが本題だ。上級エンジニアがこれらの擬似クラスを真に活用すべき場面は、単なる `` の制御ではない。`contenteditable` 属性を持つリッチテキスト領域や、インライン編集可能なDOM要素のスタイリングにおいてだ。
モダンなWebアプリでは、Notionのようなブロックエディタや、表計算のセル編集など、「クリックするとその場でテキストエリアのように編集できるUI」が求められる。ここで `contenteditable=”true”` と `contenteditable=”false”`(あるいは属性の有無)を動的に切り替える設計において、`:read-only` と `:read-write` は神掛かった挙動を示す。
以下のコードを見てほしい。
ここをクリックしてタイトルを編集
このセクションは現在、管理者によってロックされています。/ ==========================================
contenteditable要素の高度な状態管理
========================================== /.editable-card [contenteditable] {
border: 1px dashed transparent;
padding: 0.5rem;
border-radius: var(–radius-sm);
transition: background-color 0.15s, border-color 0.15s;
}/ 編集可能な状態 (:read-write)
「編集できること」をユーザーにほのめかすホバー・フォーカススタイルの提供 /
.editable-card [contenteditable]:read-write:hover {
border-color: var(–color-border-interactive);
background-color: var(–color-bg-hover);
}.editable-card [contenteditable]:read-write:focus {
outline: none;
border-color: var(–color-primary);
background-color: var(–color-bg-surface);
box-shadow: 0 0 0 2px var(–color-primary-alpha);
}/ 読み取り専用の状態 (:read-only)
contenteditableであっても、何らかの理由で読み取り専用にフォールバックした要素 /
.editable-card [contenteditable]:read-only {
background-color: transparent;
border-color: transparent;
cursor: not-allowed;
user-select: text; / テキストの選択・コピーは許可しつつ、編集はさせない絶妙な制御 /
}この手法の優れている点は、JavaScript側で「編集モードか否か」のフラグをDOMの属性(`contenteditable` や `readonly`)として正しく反映させさえすれば、見た目の制御は100%CSSエンジンにオフロードできるという点だ。フレームワークの再レンダリングサイクルからスタイル計算を完全に切り離すことで、パフォーマンスのボトルネックを未然に防ぐことができる。
—
4. パフォーマンスとメモリ効率、そして「罠」の回避策
ここで、チーフアーキテクトとして実務でハマりがちな「地雷」についても言及しておかなければならない。
罠1: `:read-only` と `:disabled` の仕様上の違い
初心者や中級者がやりがちなミスとして、`disabled` がついた要素を `:read-only` でスタイリングしようとすることが挙げられる。
仕様上、多くのブラウザでは `disabled` な要素も広義の読み取り専用として扱われることがあるが、明示的に `:disabled` と `:read-only` はセレクタのヒット範囲が異なる場合がある(特にフォームのバリデーション状態において)。堅牢性を担保するためには、セレクタを以下のように厳密に住み分けるか、フォールバックを考慮する必要がある。
/ 無効化された要素は明確に別枠で定義する /
.form-control:disabled {
background-color: var(–color-bg-disabled);
color: var(–color-text-disabled);
cursor: not-allowed;
opacity: 0.7;
}/ 明示的に readonly が指定された要素 /
.form-control:read-only:not(:disabled) {
background-color: var(–color-bg-readonly);
/ 無効化とは異なり、テキストのコピーができることを意識したスタイル /
}罠2: レンダリングエンジンの再計算コスト(CSSOMの肥大化を防ぐ)
過度に複雑なセレクタの組み合わせ(例:`.wrapper > div:not(.active) input:read-only` のような深すぎるネスト)は、DOMツリーが巨大化した際にスタイルの再計算(Style Recalculation)のコストを跳ね上げる原因になる。
ブラウザのパフォーマンスプロファイラ(Chrome DevToolsのPerformanceタブなど)を覗けばわかるが、動的な属性変化(`readonly` の付与など)が起きた際、CSSエンジンは影響を受ける要素のセレクタマッチングを再評価する。
ここで、可能な限りセレクタの階層を浅く保ち、クラス名と擬似クラスの組み合わせをシンプルに保つこと(例:`.form-control:read-only` のように単一のクラス+擬似クラスで完結させること)が、60fps(あるいは120fps)の滑らかなUIを維持するための極意である。—
5. 結び:ネイティブの力を信じよ
フレームワーク全盛の現在、私たちはともすれば「JavaScriptのステートが正義」という錯覚に陥りがちだ。しかし、Webプラットフォームの基盤であるCSSは、私たちが想像するよりも遥かにインテリジェントで、高速に最適化されている。
`:read-only` と `:read-write` は、単なる「便利な書き方」ではない。DOMのセマンティクスとスタイリングを完璧に一致させ、JavaScriptのロジックから視覚的関心事を美しく分離するための、極めて洗練されたアーキテクチャ・パターンなのだ。
次にフォームやエディタコンポーネントを設計する際は、JSの条件分岐でクラスを付け替える手を一度止め、CSSのネイティブな擬似クラスにその役割を委ねてみてほしい。コードベースが軽くなり、ブラウザが喜ぶ音が聞こえるはずだ。

コメント