ここ最近の開発では、CSS のカスタムプロパティ(CSS 変数)を活用する機会が増えていますが、calc() 関数や var() 関数を組み合わせると値を予測するのが難しくなることがあります。

具体的な例として、以下はブログ記事「CSS で学ぶ三角関数」のデモで使用したコードを改変したものです。

ここでは、コードの内容を理解する必要はありませんが、cos()sin() の 2 つの関数を使用して円周にオブジェクトを配置しており、カスタムプロパティ --angle の値に応じてオブジェクトの位置が変わるといったものです。

CSS の値に関数を使用しているコードの例
<!-- HTML -->
<div class="c-circle">
  <div class="c-circle-point"></div>
</div>

<!-- CSS -->
<style>
.c-circle {
  position: relative;
  container-type: size;
  inline-size: min(40vw, 200px);
  aspect-ratio: 1;
  border-radius: 50%;
  background-color: hsl(180 100% 23%);
}
.c-circle-point {
  --angle: 45;
  --point-size: 10cqi;
  --radius: calc(50cqi - (var(--point-size) / 2));
  --translateX: calc(cos(var(--angle) * 1deg) * var(--radius));
  --translateY: calc(sin(var(--angle) * 1deg) * var(--radius) * -1);

  position: absolute;
  inset-block-start: calc(50% - (var(--point-size) / 2));
  inset-inline-start: calc(50% - (var(--point-size) / 2));
  inline-size: var(--point-size);
  aspect-ratio: 1;
  border-radius: 50%;
  background-color: hsl(77 80% 50%);
  translate: var(--translateX) var(--translateY);
}
</style>

上記のコードを見てわかるように、calc()var() といった関数、さらには cos()sin() のような関数まで加わると、一見してどのようなスタイルが適用されるのかを把握するのは容易ではありません。

このわかりづらさを打開する方法はないものかと考え、手軽にスタイルの計算後の値を確認するための Bookmarklet「css-console」を作成しました。

今回作成した Bookmarklet は、以下のリンクをブラウザのブックマークバーにドラッグ & ドロップすることで登録できます。

css-console.js

Bookmarklet のコードは、以下の GitHub Gist にもアップしています。

Bookmarklet とは

Bookmarklet(ブックマークレット)は、ウェブブラウザのブックマークから JS のコードを実行する仕組みのことをいい、javascript: スキームを即時関数で実行することによって実現します。

javascript:(() => {
  /* 実行するスクリプト */
})();

例えば、以下は閲覧している記事の titleprompt で表示する Bookmarklet です。

javascript:(() => {
  prompt('title', document.title);
})();

まず、以下の手順で Bookmarklet を登録します。

  1. 適当なウェブページをブックマーク(このページでも可)
  2. ブックマークを編集して、URL を JS のコードに差し替える
  3. ブックマーク名をわかりやすい名前に変更(任意)

試しに、先ほどの Bookmarklet を登録して実行すると、ダイアログが表示されテキストフィールドにページの <title> が表示されます。

Bookmarklet を実行してダイアログが表示されている状態のスクリーンショット
Bookmarklet を Google Chrome で実行した例

デベロッパーツールのコンソールに、JS コード全体をペーストしても実行可能です。

なお、Bookmarklet は JS なので、以下の制約が挙げられます。

  • Content Security Policy(CSP)の指定内容によっては、スクリプトの実行が拒否される
  • ブラウザの拡張機能や、ドキュメントで読み込んでいる JS や CSS と競合する場合がある
  • <iframe> や Shadow DOM の内側には、基本的にはアクセス不可
  • 異なるオリジンのリソースには、基本的にはアクセス不可

また、ブックマークとして登録するときに 1 行のコードになるため、JS を記述するときには以下の点に気をつける必要があります。

  • 行末のセミコロン(;)を省略しない
  • コメントは複数行コメント(/* */)を使用する

Bookmarklet を実行すると、以下のウィジェットが表示されます。

ウィジェットの構成。タイトル、Close、ID、値、セレクタ、プロパティ、インスペクタ、削除、追加、再計算の項目が図で示されている
項目内容
タイトルBookmarklet の名称
Closeウィジェットを閉じるボタン
IDアイテム ID
計算済みのスタイルの値
セレクタCSS セレクタの入力フィールド
プロパティCSS プロパティの入力フィールド
インスペクタインスペクタモードに切り替えるボタン
削除アイテムを削除するボタン
追加アイテムを追加するボタン
再計算スタイルの値を再取得するボタン

「タイトル」の領域をドラッグすることで、ウィジェットを移動できます。

ウィジェットの表示・非表示

見出し「ウィジェットの表示・非表示」

「Close」ボタンを選択するとウィジェットが非表示になり、移動した位置と変更したサイズはリセットされます。ただし、入力した値はページを再読み込みしない限り維持されます。

また、Bookmarklet の JS を実行したときも同様です。実行するごとにウィジェットの表示・非表示が切り替わりますが、値は破棄されずに位置とサイズのみリセットされます。

この機能は、ウィジェットをビューポートの外側まで移動させてしまい、元の位置に戻せないときなどにも活用できます。

アイテムの追加・削除

見出し「アイテムの追加・削除」

「追加」ボタンを選択すると直後にアイテムが挿入されます。異なる HTML 要素の値を並べて表示したり、同じ要素の異なるプロパティを並べて表示したいときなどに使用します。

「削除」ボタンを選択するとそのアイテムは削除されます。

スタイルの計算済みの値を表示

見出し「スタイルの計算済みの値を表示」

この Bookmarklet のメインの機能ですが、「セレクタ」と「プロパティ」を入力すると「値」に計算済みの値が表示されます。

ウィジェットにセレクタとプロパティを入力して、計算済みの値が表示されている状態のスクリーンショット
.c-circle-pointtranslate プロパティの計算済みの値が表示されている例

「セレクタ」に入力した文字列は document.querySelector() に渡されるので、セレクタにマッチする要素が複数ある場合には、最初の要素が対象になります。例えば「p」と入力した場合には文書内で最初に出現する <p> 要素が対象になります。

もし、2 番目以降の要素を対象としたい場合や、画面内から自由に要素をピックアップしたい場合には「インスペクタ」ボタンを選択して、インスペクタモードを有効にします。

  • インスペクタモードはマウスポインタに特化しているため、キーボードやタッチデバイスでの操作はサポートしておりません。
  • ::before::after 擬似要素は、インスペクタモードでは選択できませんが、例えば「セレクタ」のフィールドに .foo::after と直接入力することで対象とすることがきます。

インスペクタモードを有効にすると、以下のようにウィジェットが一時的に非表示になり、HTML 要素を選択するモードに切り替わります。

もし、インスペクタモードを途中で終了したい場合には、ESC キーを押下します。

インスペクタモードを有効にした状態のスクリーンショット
マウスポインタの位置にあわせて対象となる要素がハイライト表示される

HTML 要素にホバーするとハイライト表示されます。そのとき、要素名と id 属性、class 属性もあわせて表示されます。要素をクリックするとウィジェットが再表示され、選択した HTML 要素のセレクタが反映されます。

ウィジェットにセレクタが反映された状態のスクリーンショット
選択した HTML 要素のセレクタが反映される

なお、この状態から「セレクタ」の値を直接変更すると、インスペクタモードで選択した要素とのリンクが解除され、document.querySelector() で取得する通常の状態に戻ります。

スタイルの値を再取得

見出し「スタイルの値を再取得」

「再計算」ボタンを選択すると、スタイルの値を再取得します。例えばサイズや位置を変更したり、ダークモードに切り替えたときに、変更後の値を取得するために使用します。

現時点では継続的に値を取得する機能は備えていません。

デベロッパーツールで計算済みの値を確認する

見出し「デベロッパーツールで計算済みの値を確認する」

スタイルの計算済みの値は、本来であればブラウザ標準のデベロッパーツールを使用して確認します。

本記事での Bookmarklet と比較すると、ブラウザ標準の機能なので当然ながらパフォーマンスに優れており、サイズや位置を変更したときなどにリアルタイムに値を取得できます。また、<iframe> や Shadow DOM の内側にもアクセス可能です。

一方で、計算済みの値にたどり着くまでの経路が長かったり、複数要素の値を同時に確認できなかったりと、ちょっとした使いづらさを感じます。

例えば、Google Chrome で特定のカスタムプロパティの計算済みの値を確認するには、以下のステップを踏む必要があります。

  1. デベロッパーツールを起動
  2. 「Elements(要素)」を選択
  3. 「Computed(計算済み)」を選択
  4. 「Show all(すべて表示)」を有効化
  5. 「Filter」で対象のカスタムプロパティを検索
Google Chrome のデベロッパーツールのスクリーンショット。1 から 5 までのステップを指し示している
Google Chrome のデベロッパーツールでカスタムプロパティの計算済みの値を確認する手順

Firefox や Safari のデベロッパーツールでは、より少ないステップでたどり着けますが、どの方法を採用したとしても一長一短があると感じています。

ウィジェットは Shadow DOM として実装しており、JS の先頭に記述しているクラスフィールドで基本設定を指定しています。

以下にその一部を抜粋しますが、#title は「タイトル」で表示されるテキストで、#inputDebounce はテキストフィールドに入力したときの処理を実行する遅延時間(ms)です。

#styles では CSS のコードをテンプレートリテラルで格納しています。

theme レイヤーで指定しているカスタムプロパティの値を変更して、ウィジェットの配色やフォント指定、基本となるサイズや余白を変更できます。

クラスフィールドの抜粋
class CSSConsole extends HTMLElement {
  /* preferences */
  #title = 'css-console';
  #inputDebounce = 600;

  /* CSS */
  #styles = `
    @layer reset, layouts, components, theme;

    @layer theme {
      :host {
        --color-bg-base    : hsl(180 0% 0%);
        --color-bg-widget  : hsl(180 10% 20%);
        --color-bg-item    : hsl(180 78% 10%);

        --color-primary    : hsl(180 17% 84%);
        --color-secondary  : hsl(180 17% 60%);
        --color-border     : hsl(180 17% 60%);
        --color-highlight  : hsl(180 12% 26%);

        --value-bg         : hsl(73 10% 70%);
        --value-fg         : hsl(180 0% 0%);
        --value-border     : hsl(180 78% 28%);

        --btn-primary-bg   : hsl(36 35% 25%);
        --btn-primary-fg   : hsl(180 17% 84%);
        --btn-secondary-bg : hsl(36 78% 54%);
        --btn-secondary-fg : hsl(180 78% 10%);

        --color-scheme: dark;

        --font-family: Menlo, Consolas, monospace;
        --font-size-m: calc(1rem * 14 / var(--rem, 16));
        --font-size-s: calc(1rem * 12 / var(--rem, 16));
        --line-height: 1.6;
        --letter-spacing: 0.05em;

        --inline-size: min(460px, 100%);
        --padding: 16px;
      }
    }
    /* ... */
  `;
};

現時点では以下の問題や課題を認識しています。

継続的に値を取得できない

見出し「継続的に値を取得できない」

現状は変更された値は継続的に取得されません。そのため、インタラクティブ性の高いコンポーネントの場合、その都度「calc」ボタンを選択する必要があり非効率です。

パフォーマンスを悪化させないように注意を払う必要はありますが、例えば「calc」ボタンを「sync」スイッチのようなものに変更して、スイッチがオンの状態では継続的に値を取得するのがよいかもしれないと考えています。

計算途中の値を確認しづらい

見出し「計算途中の値を確認しづらい」

スタイルの値は getPropertyValue() を使用して取得していますが、通常のプロパティを指定した場合には、計算済みの値しか確認することができません。

対して、カスタムプロパティ(--*)を指定すると、var() 関数のみ展開されて、calc()sin()cos() といった関数は計算されずにそのままになります。

具体的な例を見ていきます。以下は記事の冒頭で紹介した CSS コードの抜粋です。

CSS の値に関数を使用しているコードの例
.c-circle-point {
  --angle: 45;
  --point-size: 10cqi;
  --radius: calc(50cqi - (var(--point-size) / 2));
  --translateX: calc(cos(var(--angle) * 1deg) * var(--radius));
  --translateY: calc(sin(var(--angle) * 1deg) * var(--radius) * -1);

  translate: var(--translateX) var(--translateY);
}

ここで、JS の getPropertyValue() を使用して、通常のプロパティである translate と、カスタムプロパティである --translateX--translateY の値を取得します。

getPropertyValue() で、通常のプロパティとカスタムプロパティの値を取得する例
const elem = document.querySelector('.c-circle-point');

getComputedStyle(elem).getPropertyValue('translate');
// 63.6396px -63.6396px

getComputedStyle(elem).getPropertyValue('--translateX');
// calc(cos(45 * 1deg) * calc(50cqi - (10cqi / 2)))

getComputedStyle(elem).getPropertyValue('--translateY');
// calc(sin(45 * 1deg) * calc(50cqi - (10cqi / 2)) * -1)

このように、カスタムプロパティでは var() のみが展開され、計算前の状態を確認することができますが、理解しやすいとはいえません。

インスペクタモードにおけるクリックイベントの制御

見出し「インスペクタモードにおけるクリックイベントの制御」

インスペクタモードでは、リンク遷移を無効にする目的で、一時的にすべての <a> 要素に対して preventDefault() を指定しています。

しかし、ドキュメントで読み込まれている JS のイベントは防止できないため、例えばページ遷移をするクリックイベントが指定されていると、要素を選択したときに実行されてしまいます。

stopPropagation() メソッドおよび stopImmediatePropagation() メソッドなどを試してみたのですが、現状の構成ではうまくいかず、回避する方法は見つかっていません。

ブラウザをリロードしたり、別ページへ遷移するとウィジェットは消えてしまいます。

また、フレームワークを使用して開発しているときに、ファイルを更新するとブラウザで開いているページに変更箇所が反映されますが(ホットリロード)、このときに <body> 要素以下の DOM が置き換わるとウィジェットごと消えてしまいます。

この問題に対応するには、sessionStorage などを使用して、一時的に入力内容を保存する機能を実装する必要があります。

ただ、インスペクタモードで紐づけた要素は保存できないので、それらも踏まえて実装を見直すことになりそうです。

Bookmarklet が対応できないケース

見出し「Bookmarklet が対応できないケース」

<iframe> の内部(「CodePen」のコードなど)や、Shadow DOM の内側の HTML 要素は取得できないので対象外です。

加えて、前述した Content Security Policy(CSP)の指定内容や、ブラウザの拡張機能、ドキュメントで読み込まれている JS や CSS の内容によっては、正しく動作しない可能性があります。

今回作成した Bookmarklet が、実際の開発の場面で活用できるかどうかは、もう少し多くの場面で使っていかないとわかりません。また、現時点ではいくつか課題も残っているので、引き続きブラッシュアップを重ねていきたいと考えています。

本記事では、具体的な実装内容についてはあまり言及できませんでしたが、まとめられるトピックがあれば、別途ブログ記事のなかで取り上げていきます。