前回の CSS の値を確認するための Bookmarklet「css-consle」に続いて、カスタムプロパティを一覧で表示する Bookmarklet「custom-props-viewer」を作成しました。
Bookmarklet
見出し「Bookmarklet」今回作成した Bookmarklet は、以下のリンクをブラウザのブックマークバーにドラッグ & ドロップすることで登録できます。
Bookmarklet のコードは、以下の GitHub Gist にもアップしています。
https://gist.github.com/griponminds/e8def848638a5e9021eaa1a7c251427e
Bookmarklet とは
Bookmarklet(ブックマークレット)は、ウェブブラウザのブックマークから JS のコードを実行する仕組みのことをいい、javascript:
スキームを即時関数で実行することによって実現します。
javascript:(() => {
/* 実行するスクリプト */
})();
例えば、以下は閲覧している記事の title
を prompt
で表示する Bookmarklet です。
javascript:(() => {
prompt('title', document.title);
})();
まず、以下の手順で Bookmarklet を登録します。
- 適当なウェブページをブックマーク(このページでも可)
- ブックマークを編集して、URL を JS のコードに差し替える
- ブックマーク名をわかりやすい名前に変更(任意)
試しに、先ほどの Bookmarklet を登録して実行すると、ダイアログが表示されテキストフィールドにページの <title>
が表示されます。

デベロッパーツールのコンソールに、JS コード全体をペーストしても実行可能です。
なお、Bookmarklet は JS なので、以下の制約が挙げられます。
- Content Security Policy(CSP)の指定内容によっては、スクリプトの実行が拒否される
- ブラウザの拡張機能や、ドキュメントで読み込んでいる JS や CSS と競合する場合がある
<iframe>
や Shadow DOM の内側には、基本的にはアクセス不可- 異なるオリジンのリソースには、基本的にはアクセス不可
また、ブックマークとして登録するときに 1 行のコードになるため、JS を記述するときには以下の点に気をつける必要があります。
- 行末のセミコロン(
;
)を省略しない - コメントは複数行コメント(
/* */
)を使用する
使用方法
見出し「使用方法」Bookmarklet を実行すると、以下のウィジェットが表示されます。

項目 | 内容 |
---|---|
タイトル | Bookmarklet の名称 |
Close | ウィジェットを閉じるボタン |
フィルタ | カスタムプロパティの絞り込み |
カウント | カスタムプロパティの総数 |
リロード | リストを再読み込み |
チェックボックス | セレクタ(selector)、値(value)の表示・非表示切り替え |
セレクタ | CSS セレクタ |
プロパティ | カスタムプロパティ名 |
値 | スタイルの値 |
基本機能
見出し「基本機能」この Bookmarklet の基本機能としては、ドキュメントに読み込まれているスタイルシート1を解析し、カスタムプロパティの一覧とその値を表示します。
リストの各アイテムには、セレクタ・プロパティ・値のセットで表示されます。

At-rules
見出し「At-rules」@layer
、@media
、@container
といった At-rules が指定されている場合には、そのグループルールもあわせて表示されます(@supports
は割愛しています)。
var()
の展開
この見出しのリンク値に var()
が含まれる場合には、var()
を展開した値を矢印(➡)の後ろに表示しています。ただし、セレクタで指定した要素がドキュメント内に存在する場合に限られます。
スウォッチ
見出し「スウォッチ」値がカラー(<color>
)の場合には、カラースウォッチを表示しています。
ウィジェットの移動
見出し「ウィジェットの移動」「タイトル」の領域をドラッグすることで、ウィジェットを移動できます。
ウィジェットの表示・非表示
見出し「ウィジェットの表示・非表示」「Close」ボタンを選択するとウィジェットが非表示になり、移動した位置と変更したサイズはリセットされます。ただし、入力した値はページを再読み込みしない限り維持されます。
また、Bookmarklet の JS を実行したときも同様です。実行するごとにウィジェットの表示・非表示が切り替わりますが、値は破棄されずに位置とサイズのみリセットされます。
この機能は、ウィジェットをビューポートの外側まで移動させてしまい、元の位置に戻せないときなどにも活用できます。
フィルタ
見出し「フィルタ」テキストフィールドに --
(<dashed-ident>
)に続くプロパティ名を入力することで、カスタムプロパティの絞り込みができます。
一致条件はその文字が含まれるかどうかで、CSS の属性セレクタ([attr*=value]
)で実現しています。フィルタの具体的な実装内容は後述します。
リロード
見出し「リロード」「リロード」ボタンを選択すると、ドキュメントに読み込まれているスタイルシートを再度解析します。例えば非同期遷移したときやダークモードに切り替えたときに、変更後の値を取得するために使用します。
なお、スタイルシートの変化の有無に関わらず、ボタンを選択するとリストの先頭までスクロールするので、先頭に戻りたいときにも使用できます。
チェックボックス
見出し「チェックボックス」「selector」と「value」のチェックボックスを操作することで、項目の表示・非表示を切り替えることができます。両方のチェックを外すと、カスタムプロパティ名のみを一覧で確認することができます。
基本設定の変更
見出し「基本設定の変更」基本設定の変更は、前回の「css-console」と同じです。
ウィジェットは Shadow DOM として実装しており、JS の先頭に記述しているクラスフィールドで基本設定を指定しています。
以下にその一部を抜粋しますが、#title
は「タイトル」で表示されるテキストで、#inputDebounce
はテキストフィールドに入力したときの処理を実行する遅延時間(ms)です。
#styles
では CSS のコードをテンプレートリテラルで格納しています。
theme
レイヤーで指定しているカスタムプロパティの値を変更して、ウィジェットの配色やフォント指定、基本となるサイズや余白を変更できます。
class CustomPropsViewer extends HTMLElement {
/* preferences */
#title = 'custom-props-viewer';
#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 36% 60%);
--color-highlight : hsl(180 12% 26%);
--checkbox-border : hsl(180 7% 30%);
--checkbox-color : hsl(46 100% 50%);
--value-bg : hsl(180 0% 0%);
--value-fg : hsl(180 52% 72%);
--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(600px, 100%);
--padding: 16px;
}
}
/* ... */
`;
};
実装内容
見出し「実装内容」ここからは、Bookmarklet の実装内容の一部を紹介します。
カスタムプロパティの取得
見出し「カスタムプロパティの取得」JS でスタイルシートのカスタムプロパティを取得する方法は、以下の記事を参考にしました。
ドキュメントで読み込んでいるスタイルシートは document.styleSheets
で取得できます。
ここから、カスタムプロパティが指定されているスタイルを抽出していくのですが、最終的には以下のようなデータに整形しています。
[
{
"prop": "--color-accent",
"selector": ":root",
"value": "hsl(180 100% 23%)",
"layers": [],
"medias": [],
"containers": [],
"calc": ""
},
{
"prop": "--border-color",
"selector": ":root",
"value": "var(--color-accent)",
"layers": [],
"medias": [],
"containers": [],
"calc": "hsl(180 100% 23%)"
},
{
"prop": "--padding",
"selector": ".foo",
"value": "40px",
"layers": [
"components"
],
"medias": [
"(prefers-color-scheme: dark)"
],
"containers": [
"(min-inline-size: 460px)"
],
"calc": ""
},
/* ... */
]
このデータをもとに、リストの HTML を生成しています。
簡易フィルタ機能
見出し「簡易フィルタ機能」フィルタ機能は、おもに CSS の属性セレクタを使用して実装しています。
まず、JS で生成する HTML の各アイテムに、カスタムプロパティ名から --
(<dashed-ident>
)を取り除いてカスタムデータ属性 data-prop
として指定しておきます。
<div>
<label>
Filter: <input type="search" placeholder="property name">
</label>
</div>
<ul>
<li data-prop="color-accent">--color-accent</li>
<li data-prop="border-color">--border-color</li>
<li data-prop="padding">--padding</li>
</ul>
あとは、JS で <style>
要素を生成して、入力した値を属性セレクタ([attr*=value]
)に指定して、マッチしない場合には display: none
で非表示にするだけです。
const search = document.querySelector('input[type="search"]');
const style = document.createElement('style');
document.body?.append(style);
const filterProp = (value, style) => {
style.textContent = '';
if (value !== '') {
/* 属性セレクタ [attr*=value] で表示を制御 */
const stylesheet = `
[data-prop]:not([data-prop*="${value}"]) { display: none; }
`;
style.append(stylesheet);
}
};
search?.addEventListener('input', () => {
filterProp(search.value, style);
});
JS の正規表現を使用して表示を制御してもよかったのですが、今回はシンプルに実装したかったので、CSS の属性セレクタをベースにしました。
var()
の展開
この見出しのリンク前述のように、値に var()
が含まれる場合には、var()
を展開した値を併記していますが、そのとき、@media
に一致するかを window.matchMedia
で判定しています。
const calcStyleValue = (selector, prop, medias) => {
// メディアクエリに一致するかを判定
if (medias && !window.matchMedia(medias).matches) return '';
const target = document.querySelector(selector);
// 対象となる要素が存在するかを判定
if (!target) return;
// 対象となる要素のスタイルの値を返す
return getComputedStyle(target).getPropertyValue(prop);
};
calcStyleValue('button', '--border-color', '(prefers-color-scheme: dark)');
なお、@container
は window.matchMedia
に相当するメソッドが今のところ存在しない2ので、コンテナクエリに一致するかの判定はしていません。
カラーの判定
見出し「カラーの判定」値がカラー(<color>
)の場合にはスウォッチを表示していますが、カラーの判定は以下のように CSS.supports()
メソッドを使用しています。
const isColor = (value) => CSS.supports('color', value);
isColor('transparent'); // true
isColor('hsl(180 100% 23%)'); // true
isColor('100%'); // false
Safari における Shadow DOM のバグ
見出し「Safari における Shadow DOM のバグ」チェックボックスによる表示の制御を :has()
擬似クラスで実装しているときに気づいたのですが、macOS Safari では、Shadow DOM と :has()
擬似クラスのバグが存在するようです。
例えば、以下のコードはチェックボックスの :checked
に応じて、.foo
、.bar
の表示・非表示が入れ替わることを想定していますが、Safari ではチェックボックスを変更しても見た目上の変化が起こりません。
<my-component></my-component>
<script>
class MyComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
/* チェックボックスをオンにしたら非表示 */
.container:has([type="checkbox"]:checked) .foo {
display: none;
}
/* チェックボックスをオフにしたら非表示 */
.container:has([type="checkbox"]:not(:checked)) .bar {
display: none;
}
</style>
<div class="container">
<label><input type="checkbox">check</label>
<p class="foo">foo</p>
<p class="bar">bar</p>
</div>
`;
}
}
customElements.define('my-component', MyComponent);
</script>
ただ、デベロッパーツールで確認すると値自体は反映されているので、描画に反映されていないだけのようです。

当初は、チェックボックスでの表示の制御を、:has()
擬似クラスを使用した方法で実装していたのですが、このバグが発覚したので別の方法に変更しました。
この現象は、Light DOM でカスタム要素を定義した場合には発生しないため、Shadow DOM 特有の Safari のバグのようです。
おわりに
見出し「おわりに」前回に引き続き、CSS 関連の Bookmarklet を紹介しました。
Bookmarlet は古くからある手法ですが、特定の目的にあわせたツールを手軽につくりたいといったニーズを満たすことができます。また、使用する場面は限定されるので、CSS や JS の新しい機能を試すことのできる機会にもなると思っています。
今回作成した Bookmarklet に関しても、まだ改善の余地は残されていますが、ひとまず形になったので公開しました。もしお気づきの点がございましたらお知らせください。