`:read-only` 疑似クラスの深層:フォームの境界線を制するアーキテクチャ
こんにちは。日々、CSSのセレクタエンジンとブラウザの再描画パイプラインの最適化に情熱を燃やすフロントエンド・アーキテクトの私だ。
モダンなWebアプリケーションのUIを設計する際、私たちは常に「状態(State)」との戦いを強いられている。ReactやVueといったフレームワーク全盛の時代であっても、最終的なピクセルを描画し、ユーザーインタラクションのパフォーマンスを担保するのはブラウザのCSSエンジンに他ならない。
特に、デザインシステムや堅牢なフォームUIを構築する際、頭を悩ませるのが「編集可能な要素」と「読み取り専用(Read-only)の要素」の境界線だ。JavaScriptで状態を管理し、わざわざ `is-readonly` などの冗長なユーティリティクラスをDOMに付与していないだろうか?
もしそうなら、今すぐその実装を見直してほしい。ブラウザには、ネイティブでこの状態を完璧に追跡し、メモリ効率よくスタイリングを適用できる強力なプリミティブが存在する。それが今回深掘りする `:read-only` 疑似クラスだ。
単なる「編集不可な要素にグレーの背景を敷くだけのセレクタ」と思って使っているなら、非常にもったいない。ブラウザの内部挙動、メモリ効率、そして非同期レンダリングの競合を防ぐためのアーキテクチャ的アプローチを、ギークな視点から徹底的に紐解いていこう。
—
`:read-only` の正体:ブラウザは何を監視しているのか
まず、CSSにおける `:read-only` と `:read-write` の仕様を正確に理解する必要がある。HTMLスペシフィケーションにおいて、どの要素が「読み取り専用」と判定されるのか。その判定基準は、実は単純な `readonly` 属性の有無だけにとどまらない。
ブラウザの内部エンジン(Blink, Gecko, WebKit)は、要素が次の条件のいずれかに合致する場合に「read-only」フラグを立てる。
1. `input` または `textarea` 要素で、`readonly` 属性が存在し、かつ `disabled` 属性がない場合。(※`disabled` の要素には `:disabled` が優先的に適用され、通常 `:read-only` の対象外となるか、ブラウザのスタイルシートで別途扱われる)
2. `input` 要素で、`readonly` 属性を持たないが、テキスト入力以外の型(例: `checkbox`, `radio`, `file`, あるいは各種ボタン類)であり、かつ `disabled` でない場合。
3. `contenteditable=”false”` が指定された要素、またはデフォルトで編集不可な通常の要素(例: `
特筆すべきは2点目だ。チェックボックスやラジオボタンは、デフォルトで `:read-only` の判定を受ける。 これを知っているだけでも、CSSセレクタの設計思想がいかに洗練されるかお分かりいただけるだろう。
メモリ効率とセレクタの最適化
CSSセレクタの評価において、疑似クラスはDOMツリーの走査コストに影響を与える。しかし、`:read-only` はブラウザが要素の内部状態(State)として保持しているフラグを直接参照するため、クラスセレクタ `.is-readonly` をJS側で毎度DOMにトグルさせる手法に比べ、以下の圧倒的なアドバンテージを持つ。
- DOMの肥大化防止: JSによるclass属性の書き換えが発生しないため、React等の仮想DOMの差分検出(Reconciliation)における属性変更コストがゼロになる。
- メモリリークの根絶: イベントリスナーや不要なステート管理が不要になり、ブラウザのスタイル計算キャッシュ(Style Resolution Cache)を効率的にヒットさせられる。
—
実践的アーキテクチャ:堅牢なフォームデザインシステムでの活用
では、実際のエンタープライズ向けデザインシステムにおいて、どのように `:read-only` を組み込むべきか。ありがちな「ダサい見た目」を回避し、アクセシビリティ(a11y)を担保したプロダクションコードを見てみよう。
/ =================================インプット要素のベース設計 ================================= /
.form-control {
/ 共通のベーススタイル /
display: block;
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
line-height: 1.5;
color: var(–color-text-primary);
background-color: var(–color-bg-surface);
border: 1px solid var(–color-border-default);
border-radius: var(–radius-md);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
/ フォーカス時の振る舞い(編集可能な場合のみ) /
.form-control:focus:not(:read-only) {
outline: none;
border-color: var(–color-primary);
box-shadow: 0 0 0 3px var(–color-primary-alpha);
}
/ =================================:read-only の適用領域 ================================= /
.form-control:read-only {
/
【重要】
ユーザーに「ここは触れない(が、データとしては存在する/コピー可能)」という文脈を伝えるため、
カーソルを変更し、背景色をトーンダウンさせる。
disabled(無効化)とは異なり、テキストの選択やコピー&ペーストは許可するため、
user-select はあえて制御しないのがポイント。
/
cursor: default;
background-color: var(–color-bg-muted);
border-color: var(–color-border-subtle);
color: var(–color-text-muted);
/ フォーカスリングを完全に排除するか、意図的なものに制限する /
box-shadow: none;
}
/
アクセシビリティ配慮:
スクリーンリーダーやキーボードナビゲーションでフォーカスが当たった際、
read-onlyであってもフォーカスが見えなければならない場合がある。
その際のスタイリングを明示的に定義する。
/
.form-control:read-only:focus {
outline: 1px dotted var(–color-text-muted);
outline-offset: 2px;
}
このアプローチの美しいところは、`:focus:not(:read-only)` という複合セレクタにある。JavaScript側で「この入力欄は現在編集可能か?」を判定してイベントハンドラやクラスを切り替える必要が一切ない。HTMLの `readonly` 属性が変化した瞬間、CSSエンジンが自律的にスタイルを切り替えるのだ。
—
非同期の競合と重大なバグの回避策
上級エンジニアであれば、ここで一つ懸念を抱くはずだ。
「非同期でフォームの初期値やバリデーション結果が流し込まれ、同時に `readonly` 属性が動的に書き換わる瞬間、スタイルとインタラクションの間に競合(Race Condition)は起きないのか?」と。
レンダリングパイプリンの競合とFOUC(スタイル未適用の一瞬の露呈)の防止
シングルページアプリケーション(SPA)において、APIから取得したデータをもとに、フォーム要素の `readonly` が `false` から `true` へ、あるいはその逆に非同期で切り替わるケースは多々ある。
この時、JavaScriptのステート更新とブラウザの再描画(Reflow/Repaint)のタイミングによっては、「編集できるはずなのに一瞬readonlyの見た目になっている」、あるいはその逆の現象(FOUC)が発生し、ユーザーにストレスを与える。
これを防ぐためのアーキテクチャ上の解法は、「初期状態のマークアップから `readonly` を正確にサーバサイドまたは初期SSRの段階でレンダリングし、クライアントサイドでの不要なレイアウトシフトを排除すること」だ。
さらに、CSS側で状態の不整合を防ぐための防護壁を張る。
/
データロード中のスケルトン状態や、非同期処理の競合による
「中途半端な状態」を防ぐためのガードスタイル
/
.form-control:read-only[aria-busy=”true”] {
background-image: linear-gradient(
90deg,
var(–color-bg-muted) 0%,
var(–color-skeleton-highlight) 50%,
var(–color-bg-muted) 100%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`:disabled` との混同によるバグ
現場で最も頻繁に見るバグの一つが、`:disabled` と `:read-only` のスタイルの混同だ。
- `:disabled`: 要素が無効化されている。フォームの送信データに含まれず、フォーカスも当たらない。クリックイベントも発火しない。
- `:read-only`: 要素は「読み取り専用」である。フォームの送信データには含まれる。フォーカス可能であり、テキストの選択やコピーも可能。
このセマンティクスの違いを無視して、どちらも同じ「グレーアウトされた入力欄」にしてしまうと、ユーザーは「このデータは送信されるのか?」「コピーできないのか?」という混乱に陥る。
バックエンドへデータを送信するプレビュー画面などで、ユーザーに内容を確認させつつコピーさせたい場合は、必ず `:read-only` を採用し、次のような明確な視覚的フィードバックを設計に組み込むべきだ。
/ コピー可能であることを視覚的に示唆するユーティリティの併用 /
.form-control:read-only::after {
/ ※Pseudo-elementsはinput等の置換要素(Replaced elements)には直接適用できないため、
実際にはラッパー要素で制御するか、アイコンを兄弟要素として配置するアーキテクチャをとる /
}
.input-wrapper:has(input:read-only) .copy-hint-icon {
opacity: 1;
pointer-events: auto;
}
ここで登場した `:has()` 疑似クラスと `:read-only` の組み合わせは、モダンCSSアーキテクチャの真骨頂だ。親要素側で「子孫にread-onlyのインプットが存在する」という文脈を検知し、外側のコンテナデザイン全体を変化させることが可能になる。
—
パフォーマンス最適化の極み:巨大フォームにおけるスケーリング
数百フィールドを超えるような巨大なエンタープライズ向けデータグリッドや管理画面を想像してほしい。すべてのセルや入力欄にJSのイベント監視やクラスバインディングを行ったら、メインスレッドは悲鳴を上げ、入力遅延(Input Latency)が跳ね上がる。
`:read-only` を軸にしたスタイリングは、このスケーラビリティの問題に対するCSSからの直接的な回答だ。
1. JSの実行コスト削減: 状態管理のコードを極限まで削ぎ落とし、HTMLの属性だけで完結させる。
2. ペイントの局所化: ブラウザは `readonly` 属性の変更のみを検知してスタイルを再計算するため、無駄なレイアウトツリー全体の再構築(Global Layout Tree Rebuild)を回避できる。
これが、単なる「便利な疑似クラス」を超えた、アーキテクティングとしての `:read-only` の価値である。
—
結びにかえて
フレームワークの機能やJavaScriptのステート管理に頼り切るのではなく、ブラウザがネイティブで持っている強力なプリミティブ(`:read-only` や `:has()`)を信頼し、適切に組み合わせること。それこそが、時代が変わっても色褪せない、堅牢でハイパフォーマンスなWebフロントエンドを構築するための唯一無二の王道だ。
次のスプリントでは、あなたのコードベースにある無駄な `is-readonly` クラスを剥ぎ取り、ネイティブのパワーに処理を委譲してみてはどうだろうか。ブラウザは、驚くほど軽快に応えてくれるはずだ。

コメント