選択に応じてコントロール、テキスト、動作の一貫性を保つ必要がある場合は、コンポーネントの状態を更新します。見た目だけを変える場合はCSSをオーバーライドします。このガイドでは、MacのSafariでローカルのタイルプレビューを使い、両方の方法の選び方と確認方法を説明します。

読者が行う作業は明確です。自分で作成した小さなWebページを変更し、形状を選ぶコントロールの連動を保ちながら、装飾スタイルを簡単に編集できるようにします。ここでいうコンポーネントの状態とは、ページ独自の更新関数が管理するJavaScriptオブジェクトです。フレームワークや特定の製品の編集機能は前提としません。

どの変更をコンポーネントの状態に含めるべき?

変更後に何が一致している必要があるかを考えます。たとえば、丸いタイルを選んだときにチェックボックス、プレビュー、説明文も更新する必要があるなら、その選択を状態の単一の管理元にします。その値から3つの出力をすべて導出します。これは実装上の推奨事項です。選択と画面上の結果との関係が明確になります。

--tile-sizeのようなCSSカスタムプロパティには、別の役割があります。var()で参照する宣言に値を渡します。カスタムプロパティはカスケードに参加し、通常は継承されます。JavaScriptで設定した場合も、その動作はCSSのルールに従います。CSSカスタムプロパティ仕様でこの仕様が定められています。

編集を始める前に、次の判断表を使ってください。

求められる変更 推奨する管理元 この例での理由
読者が丸いタイルか四角いタイルを選ぶ コンポーネントの状態 チェックボックス、説明文、形状を一致させる必要がある
読者がタイルのサイズを選ぶ コンポーネントの状態。CSSに反映する スライダー、説明文、寸法が同じ値を共有する
すべてのタイルに異なる装飾色を使う スタイルシートの宣言 選択値やテキストを更新する必要がない
プレビューの周囲に余白を増やす スタイルシートの宣言 タイルの周りのレイアウトを変える
調査中、一時的に寸法を変える ローカルのCSSオーバーライド 後で削除する視覚的な実験である

この区別は作業内容によって変わります。このプレビューでは色は固定の装飾なのでCSSに置いています。後で色選択機能と選択色の説明を追加するなら、その選択を状態として扱い、見た目に反映します。同じ選択肢に対して、独立して編集できる値を2つ作らないようにします。

ネイティブコントロールにも独自の状態があります。チェックボックスでは、現在のcheckedプロパティを読み取ります。HTMLのchecked属性が指定するのは初期状態であり、ユーザーが変更した後の状態は追跡しません。この違いはMDNのチェックボックスプロパティのリファレンスで説明されています。

Macで自己完結したプレビューを作る

プレーンテキストのコードエディタを使い、以下の完全なドキュメントをtile-preview.htmlとして保存し、Safariで開きます。.html拡張子を付けてください。このサンプルはCSSとJavaScriptをファイル内に含み、システムフォントを使い、外部アセットなしで単純な幾何学図形のタイルを描画します。

スタイルシートはドキュメントのhead内にあります。HTMLではstyle要素をCSSの埋め込みに使い、追加リソースを参照するスタイルシートと、ドキュメント内で完結するスタイルを区別しています。この例を自己完結させることで、スタイルに影響する入力を明確にできます。HTML Standardのstyleおよびscriptingのセクションを参照してください。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Tile preview</title>
  <style>
    body {
      font-family: system-ui, sans-serif;
      margin: 2rem;
      color: #222;
      background: #fff;
    }
    label { display: block; margin-block: 1rem; }
    .preview { padding: 24px; background: #eee; }
    .tile {
      width: 80px;
      height: 80px;
      border-radius: 0;
      width: var(--tile-size, 80px);
      height: var(--tile-size, 80px);
      border-radius: var(--tile-radius, 0);
      background-color: #547899;
    }
  </style>
</head>
<body>
  <h1>Tile preview</h1>
  <label>
    <input id="round" type="checkbox"> Round tile
  </label>
  <label>
    Tile size
    <input id="size" type="range" min="40" max="160"
           step="1" value="80">
  </label>
  <button id="reset" type="button">Reset choices</button>
  <p id="summary"></p>
  <div class="preview">
    <div id="tile" class="tile" aria-hidden="true"></div>
  </div>
  <script>
    const state = { rounded: false, size: 80 };
    const round = document.getElementById('round');
    const size = document.getElementById('size');
    const summary = document.getElementById('summary');
    const tile = document.getElementById('tile');
    const reset = document.getElementById('reset');

    function render() {
      round.checked = state.rounded;
      size.value = String(state.size);
      tile.style.setProperty('--tile-size', `${state.size}px`);
      tile.style.setProperty('--tile-radius', state.rounded ? '50%' : '0');
      summary.textContent = `Tile A: ${state.rounded ? 'round' : 'square'}, ${state.size}px.`;
    }

    function update(patch) {
      const next = { ...state, ...patch };
      if (typeof next.rounded !== 'boolean' ||
          !Number.isInteger(next.size) ||
          next.size < 40 || next.size > 160) {
        throw new RangeError('Use a boolean and a size from 40 to 160.');
      }
      Object.assign(state, next);
      render();
    }

    round.addEventListener('change', () => {
      update({ rounded: round.checked });
    });
    size.addEventListener('input', () => {
      update({ size: Number(size.value) });
    });
    reset.addEventListener('click', () => {
      update({ rounded: false, size: 80 });
    });
    render();
  </script>
</body>
</html>

ここで使うスタイル操作の仕様は文書化されています。element.style.setProperty()はカスタムプロパティを含むインライン宣言を書き込みます。プロパティ名には--tile-sizeのようなCSS表記を使います。MDNのsetPropertyリファレンスを参照してください。

Round tileを選択し、Tile sizeを別の値に動かしてから、Reset choicesを選んでみてください。最初の2つの操作では形状と説明文の両方が更新され、リセットすると四角形で80ピクセルに戻るはずです。このプログラムでは、stateのフィールドに値を代入するだけではrender()は呼び出されません。update()を通じて変更を処理し、検証と描画をまとめて行います。

Nodeで、モックのコントロールとモックのスタイルオブジェクトを使い、初期値、変更後の値、不正なサイズ、リセットについて更新と描画のロジックを確認しました。チェックは通りましたが、ブラウザでの描画を保証するものではありません。Safariでファイルを開き、上記の目視チェックを行ってください。

CSS宣言をオーバーライドするのはどんなとき?

装飾を恒久的に変える場合は、保存済みのスタイルシートを編集します。たとえば.tileのbackground-colorを別のCSS色にするか、.previewのpaddingを調整します。このプログラムでは、どちらも選択された項目を表すものではないため、新しい状態フィールドは必要ありません。

一時的な実験を行う場合は、SafariのSafari → Settings… → Advanced → Show features for web developersからインスペクタを有効にします。Develop → Show Web Inspectorを開くか、Option-Command-Iを押します。これらはWebKitのインスペクタガイドに記載されたMacでの手順です。

サンプルを開いた状態で、次の完全なコンソールスニペットを実行すると、タイルの色が一時的に変わります。その後、このオーバーライドを削除する関数を使えます。

const previewTile = document.getElementById('tile');
if (!previewTile) throw new Error('Open tile-preview.html first.');
previewTile.style.setProperty('background-color', '#8069a6');
function restoreTileColor() {
  previewTile.style.removeProperty('background-color');
}
// Run restoreTileColor() when finished comparing colors.

このインライン宣言を削除すると、残りのカスケードによって色が再び決まります。スタイルシートの色指定が削除されるわけではありません。削除方法はMDNのremovePropertyリファレンスに記載されています。この例で再読み込み後も選択した色を保持するには、CSSソースを編集して保存します。

レンダラーが管理する値には、一時的なオーバーライドを使わないでください。--tile-sizeを手動で変更しても、説明文には状態の値が表示されたままで、次にrender()を呼び出すと実験した値は上書きされます。コンポーネント全体を一致させるには、コントロールまたは更新関数からサイズを変更します。

オーバーライドが効かないように見えるのはなぜ?

セレクターの詳細度を上げる前に、値がどのように適用されるかを確認します。宣言が有効でもカスケードで負けることがあります。また、勝った宣言が使っていないカスタムプロパティを対象にしている場合もあります。

  1. 対象を確認する。 形状を表示している要素を選択したことを確認します。
  2. 入力を確認する。 この例では、サイズは40から160までの整数である必要があります。不正な更新は、状態を変更する前にエラーになります。
  3. 値の使われ方を確認する。 タイルの寸法はvar(--tile-size)を使います。直接指定されたwidthがカスケードに勝つと、幅ではこの値が使われなくなる一方、高さでは引き続き使われることがあります。
  4. 優先順位を確認する。 適用される宣言、important指定、レイヤー、詳細度を比較します。
  5. 次の操作後も確認する。 コントロールを動かした後に変更が消える場合は、render()が何を書き込んでいるかを調べます。

通常のインライン宣言は、通常の作成者スタイルシートのルールより優先されます。ただし、important指定、アニメーション、トランジションは全体の優先順位に影響します。後にある宣言が自動的に勝つわけではなく、詳細度の高いセレクターでもすべての競合を解決できるとは限りません。MDNのカスケードガイドで、これらの条件が説明されています。

また、インライン値と解決済みの値を区別します。element.styleはインラインスタイルを返すため、インライン値が空でも、その要素にスタイルがないとは限りません。getComputedStyle()は、適用中のスタイルシートを反映した解決済みの値を読み取ります。MDNのstyleリファレンスとgetComputedStyleリファレンスを参照してください。

サンプルページのコンソールで、次の完全な診断用スニペットを実行します。

const inspectedTile = document.getElementById('tile');
if (!inspectedTile) throw new Error('Open tile-preview.html first.');
console.table({
  inlineSizeToken: inspectedTile.style.getPropertyValue('--tile-size'),
  inlineWidth: inspectedTile.style.getPropertyValue('width'),
  resolvedWidth: getComputedStyle(inspectedTile).width,
  resolvedHeight: getComputedStyle(inspectedTile).height
});

変更していないサンプルを開いた直後に期待される値は、インラインのサイズトークンが80px、インラインの幅が空、解決済みの寸法が80pxです。これはインラインのカスタムプロパティがスタイルシートの宣言に値を渡せる一方で、インラインのwidth宣言は作らないことを示しています。

ブラウザーやリソースのどんな制約に注意する?

MDNによると、CSSカスタムプロパティは2017年4月以降、幅広いブラウザーで利用できます。ただし、すべての旧バージョンでの対応を示すものではありません。このサンプルではvar()宣言の前に通常の寸法宣言を置いているため、カスタムプロパティが使えない場合もタイルは静的に表示されます。MDNのカスタムプロパティリファレンスを参照してください。

var(--tile-size, 80px)内のフォールバックは、カスタムプロパティを解釈できないブラウザーへの互換性を提供しません。また、入力されたすべてのトークンを検証するわけでもありません。たとえば、定義済みの値であるblueは、フォールバックがあるだけでは有効な幅になりません。これらの制約はMDNのカスタムプロパティガイドで説明されています。

対象となる旧ブラウザーがカスタムプロパティに対応していない場合は、レンダラーを変更し、setProperty()で通常のインラインwidth、height、border-radius宣言を書き込む方法が使えます。同じ状態検証とコントロールの同期を維持してください。これは描画時の仕様を変えるため、競合するレンダラーの書き込みを削除し、同じ対応状況だと決めつけずに対象ブラウザーで確認します。

このローカルプレビューでの実用的な使い分けは明確です。形状とサイズの選択には状態を使い、少数のCSS入力を通じて選択内容を描画し、装飾はスタイルシートに置きます。変更後は、コントロール操作とリセットを試します。プレビュー、コントロール、説明文に同じ選択値が反映されていることを確認してください。

参考資料