JavaScript から要素のスタイルを調べようとして element.style.color と書いたのに、空文字しか返ってこなかった経験はないでしょうか。element.style は HTML の style 属性に直接書かれた値しか持っていないため、CSS ファイルで指定したスタイルはここには現れません。
実際に画面に適用されている値を知りたいときに使うのが window.getComputedStyle() です。この記事では、基本的な使い方、返ってくる値が「計算後の値」である意味、擬似要素や CSS カスタムプロパティの読み取り方、そしてパフォーマンス上の注意点までを解説します。
目次
element.style では CSS ファイルの指定が取れない
まず、何が問題なのかを確認します。次のような HTML と CSS があるとします。
<div class="box" style="margin-top: 8px;">対象の要素</div>
.box {
width: 50%;
color: #ffffff;
background: #007bff;
font-size: 0.875em;
}
この要素に対して element.style を読むと、こうなります。
const box = document.querySelector('.box');
console.log(box.style.marginTop); // '8px' ← style 属性に書いてある
console.log(box.style.color); // '' ← CSS ファイルの指定は見えない
console.log(box.style.width); // ''
box.style が表しているのは style 属性そのもの(インラインスタイル)です。CSS ファイルやスタイルシートの指定、ブラウザーの既定スタイル、継承された値はいっさい含まれません。だからこそ box.style.color = 'red' と書けばスタイルを当てられるわけですが、読み取りの用途には使えないのです。
getComputedStyle の基本的な使い方
window.getComputedStyle() に要素を渡すと、あらゆる指定を合成したあとの、実際に適用されているスタイルが返ります。
const box = document.querySelector('.box');
const style = window.getComputedStyle(box);
console.log(style.color); // 'rgb(255, 255, 255)'
console.log(style.fontSize); // '14px'
console.log(style.marginTop); // '8px'
// getPropertyValue() を使う書き方(CSS と同じケバブケース)
console.log(style.getPropertyValue('font-size')); // '14px'
戻り値は CSSStyleDeclaration というオブジェクトです。値の取り出し方は2通りあり、どちらでも同じ結果が得られます。
| 取り出し方 | プロパティ名の書き方 | 備考 |
|---|---|---|
プロパティとしてアクセスstyle.fontSize | キャメルケース(fontSize、backgroundColor) | 短く書けるが、カスタムプロパティは取れない |
getPropertyValue('font-size') | CSS と同じケバブケース(font-size) | プロパティ名を変数で渡せる。カスタムプロパティも取れる |
なお、getComputedStyle は window のメソッドですが、グローバルスコープでは window. を省いて getComputedStyle(box) と書いても動きます。
下のデモで、同じ要素に対する element.style と getComputedStyle() の違いを見比べてみてください。margin-top だけは style 属性に書いてあるので両方に出てきますが、それ以外は element.style 側が空になります。
<div class="wrap">
<div class="box" id="box" style="margin-top: 8px;">対象の要素</div>
<button type="button" id="check">スタイルを取得する</button>
<table class="result">
<thead>
<tr><th>プロパティ</th><th>element.style</th><th>getComputedStyle</th></tr>
</thead>
<tbody id="output">
<tr><td colspan="3" class="empty">ボタンを押してください</td></tr>
</tbody>
</table>
</div>
body {
margin: 0;
padding: 16px;
font-family: system-ui, sans-serif;
font-size: 16px;
background: #f7f7f9;
}
/* ここは CSS ファイル側の指定。element.style からは見えない */
.box {
width: 50%;
padding: 1.5em;
color: #ffffff;
background: #007bff;
border-radius: 8px;
font-size: 0.875em;
}
button {
margin: 12px 0;
padding: 8px 16px;
border: 1px solid #007bff;
border-radius: 4px;
background: #fff;
color: #007bff;
cursor: pointer;
}
.result {
width: 100%;
border-collapse: collapse;
font-size: 13px;
background: #fff;
}
.result th,
.result td {
padding: 6px 8px;
border: 1px solid #ddd;
text-align: left;
}
.result th {
background: #f0f0f3;
}
.empty {
color: #888;
}
const box = document.getElementById('box');
const output = document.getElementById('output');
document.getElementById('check').addEventListener('click', () => {
// style 属性の内容しか持っていないオブジェクト
const inline = box.style;
// 実際に適用されている計算後のスタイル
const computed = window.getComputedStyle(box);
const rows = ['width', 'fontSize', 'color', 'marginTop'].map((prop) => {
const a = inline[prop] || '(空文字)';
const b = computed[prop];
return `<tr><td>${prop}</td><td>${a}</td><td>${b}</td></tr>`;
});
output.innerHTML = rows.join('');
});
返ってくるのは「計算後の値」
デモの結果を見ると、CSS に書いた文字列がそのまま返っているわけではないことに気づきます。getComputedStyle() が返すのは、ブラウザーが計算を終えたあとの解決済みの値です。
em や % は px に変換される
相対的な単位は、実際のレイアウト結果にもとづいて絶対値(多くは px)に解決されます。親の font-size が 16px なら font-size: 0.875em は '14px' として返りますし、width: 50% は親の幅に応じた '320px' のような値になります。
const style = getComputedStyle(box);
// CSS には width: 50%; padding: 1.5em; と書いてあるが……
console.log(style.width); // '320px'
console.log(style.paddingTop); // '21px'
// 数値として使いたいときは parseFloat でどうぞ
const width = parseFloat(style.width); // 320
返るのは必ず単位付きの文字列なので、計算に使うときは parseFloat() で数値に変換します。Number(style.width) では '320px' が数値にならず NaN になるので注意してください。
色は rgb() / rgba() の形式に揃えられる
色の値は、CSS にどう書いたかに関わらず rgb() 形式の文字列に正規化されます。#ffffff と書いても white と書いても、返るのは 'rgb(255, 255, 255)' です。透明度が付いている場合は 'rgba(0, 0, 0, 0.5)' のようになります。
そのため「この要素の色が #ffffff かどうか」を文字列比較で判定するコードは動きません。比較したいときは、期待値のほうも同じ形式に揃えるか、数値を抜き出して比べる必要があります。
const color = getComputedStyle(box).color; // 'rgb(255, 255, 255)'
console.log(color === '#ffffff'); // false(形式が違うので一致しない)
// 数値だけ取り出して比較する
const [r, g, b] = color.match(/\d+/g).map(Number);
console.log(r, g, b); // 255 255 255
ショートハンドより個別プロパティのほうが確実
仕様が「解決値を返す」と定めているのは margin-top や border-top-width といった個別プロパティ(ロングハンド)です。margin や border のようなショートハンドは、個別プロパティの値をブラウザーがつなぎ直して返す形になるため、期待する書式で返るとは限りません。値を確実に取りたい場面では、ロングハンドで指定するのが安全です。
擬似要素のスタイルを取得する
getComputedStyle() の第2引数に擬似要素の名前を文字列で渡すと、::before や ::after のスタイルを読み取れます。擬似要素は DOM に存在しないので、querySelector('.card::before') のような書き方はできません。これが唯一の手段です。
const card = document.querySelector('.card');
const before = window.getComputedStyle(card, '::before');
console.log(before.content); // '"NEW"' ← 引用符も含まれる
console.log(before.backgroundColor); // 'rgb(232, 84, 63)'
// 引用符を外して中身だけ取り出す
const text = before.content.replace(/^"|"$/g, ''); // 'NEW'
気をつけたいのが content の値です。CSS で content: "NEW" と書いた場合、返ってくる文字列は引用符を含んだ '"NEW"' になります。表示されている文字だけが欲しいときは、上の例のように引用符を取り除いてください。
擬似要素が定義されていない要素に対して呼んでも例外にはならず、content は 'none' が返ります。第2引数は ':before' のようにコロン1つで書いても受け付けられます。
CSS カスタムプロパティ(CSS変数)を読み取る
テーマカラーなどを CSS カスタムプロパティで管理していると、その値を JavaScript 側でも使いたくなることがあります。カスタムプロパティは getPropertyValue() で名前をそのまま(-- 付きで)指定して取得します。
// :root で定義した変数は html 要素から読む
const root = getComputedStyle(document.documentElement);
const color = root.getPropertyValue('--main-color').trim();
console.log(color); // '#e8543f'
// プロパティとしてのアクセスは使えない
console.log(root.mainColor); // undefined
console.log(root['--main-color']); // undefined
ポイントは3つあります。
1つ目は、getPropertyValue() でしか取れないことです。キャメルケースのプロパティアクセスは通常の CSS プロパティ専用なので、カスタムプロパティでは undefined になります。
2つ目は、先頭に空白が入ることがある点です。CSS で --main-color: #e8543f; と書くと、コロンのあとの半角スペースまで値の一部として扱われ、' #e8543f' のように返ることがあります。そのまま element.style.color に代入したり文字列比較したりすると意図どおりに動かないので、取得したら必ず trim() する習慣を付けてください。
3つ目は、値は書いたままの文字列で返ることです。通常のプロパティと違い、カスタムプロパティの中身はブラウザーが解釈しないただのトークン列なので、色を #e8543f と書けば rgb() には変換されず、12px と書けば 12px のまま返ります。
要素ごとに上書きしている場合は、その要素を渡せばその要素に効いている値が取れます。変数の書き換えは読み取りと対になる setProperty() で行います。
// 書き込むときは element.style 側の setProperty を使う
document.documentElement.style.setProperty('--main-color', '#1a73e8');
// 読み直すと、上書きした値が返る
const next = getComputedStyle(document.documentElement)
.getPropertyValue('--main-color')
.trim();
console.log(next); // '#1a73e8'
次のデモでは、カスタムプロパティの取得(trim する前と後の違い)と、::before のスタイル取得をまとめて試せます。
<div class="card" id="card">
<p class="title">テーマカラーを読み取る</p>
<p class="note">CSS 側で --main-color と --radius を定義しています。</p>
</div>
<button type="button" id="read">CSS変数と ::before を読み取る</button>
<ul class="log" id="log"></ul>
:root {
--main-color: #e8543f;
--radius: 12px;
}
body {
margin: 0;
padding: 16px;
font-family: system-ui, sans-serif;
background: #f7f7f9;
}
.card {
padding: 16px;
border: 2px solid var(--main-color);
border-radius: var(--radius);
background: #fff;
}
.card::before {
content: "NEW";
display: inline-block;
margin-bottom: 8px;
padding: 2px 8px;
border-radius: 4px;
background: var(--main-color);
color: #fff;
font-size: 12px;
}
.title {
margin: 0 0 4px;
font-weight: bold;
}
.note {
margin: 0;
font-size: 13px;
color: #666;
}
button {
margin: 12px 0;
padding: 8px 16px;
border: 1px solid #333;
border-radius: 4px;
background: #fff;
cursor: pointer;
}
.log {
margin: 0;
padding: 12px 12px 12px 28px;
border-radius: 6px;
background: #fff;
font-size: 13px;
line-height: 1.8;
}
const card = document.getElementById('card');
const log = document.getElementById('log');
document.getElementById('read').addEventListener('click', () => {
const style = window.getComputedStyle(card);
// カスタムプロパティは getPropertyValue でしか取れない
const raw = style.getPropertyValue('--main-color');
const color = raw.trim();
// 第2引数に擬似要素を渡すと ::before のスタイルが取れる
const before = window.getComputedStyle(card, '::before');
const lines = [
`--main-color(そのまま): "${raw}"`,
`--main-color(trim 後): "${color}"`,
`--radius: ${style.getPropertyValue('--radius').trim()}`,
`border-radius の計算値: ${style.borderTopLeftRadius}`,
`::before の content: ${before.content}`,
`::before の background-color: ${before.backgroundColor}`,
];
log.innerHTML = lines.map((line) => `<li>${line}</li>`).join('');
});
display: none の要素から値を取ろうとすると
getComputedStyle() は非表示の要素に対しても例外を投げず、ちゃんとオブジェクトを返します。ただし返ってくる値の性質が変わります。
幅や高さ、位置といったプロパティが px の絶対値で返るのは、その要素がレイアウトされていて実際の寸法が決まっているからです。display: none の要素はボックスを生成せず、レイアウトの計算対象になりません。そのため、CSS に書いた値がそのままの形で返ります。
const panel = document.querySelector('.panel'); // CSS: width: 50%; display: none;
const style = getComputedStyle(panel);
console.log(style.display); // 'none'
console.log(style.width); // '50%' ← px に解決されない
console.log(style.height); // 'auto'
// 色やフォントなど、レイアウトに関係しない値は通常どおり取れる
console.log(style.color); // 'rgb(51, 51, 51)'
「開く前にパネルの高さを測っておきたい」といった処理でこれにはまります。height が 'auto' なので parseFloat() すると NaN になり、アニメーションが動かない、という不具合につながります。
寸法を測りたいなら、いったん表示状態にしてから測るのが確実です。見た目に出したくない場合は、display を戻したうえで visibility: hidden と position: absolute を併用し、測り終えたら元に戻します。要素が document に追加されていない場合も同様に、レイアウトが存在しないので px では返りません。
取得した値は書き換えられない
getComputedStyle() が返す CSSStyleDeclaration は読み取り専用です。element.style と同じ型のオブジェクトなので見た目は書き換えられそうですが、実際にはスタイルは変わりません。
const style = getComputedStyle(box);
style.color = 'red'; // 効かない
style.setProperty('color', 'red'); // NoModificationAllowedError
// スタイルを変えたいときは element.style 側を操作する
box.style.color = 'red'; // これは効く
// もしくはクラスを付け替える
box.classList.add('is-active');
プロパティへの代入は黙って無視されますが、setProperty() を呼ぶと NoModificationAllowedError という例外になります。「読むのは getComputedStyle()、書くのは element.style かクラス」と役割を分けて覚えておくと迷いません。
なお、返ってきたオブジェクトはその時点のスナップショットではなく、要素と結びついた生きたオブジェクトです。あとからクラスを付け替えると、同じ変数を読み直したときに新しい値が返ります。
ループの中で呼ぶと極端に遅くなる
getComputedStyle() は手軽に使える反面、パフォーマンスの落とし穴があります。多くの要素を扱う処理でカクつきが出たら、まずここを疑ってください。
読み取りがレイアウトを強制するから
ブラウザーは、スタイルの変更をすぐには画面に反映せず、まとめて処理するために溜め込みます。ところが getComputedStyle() で寸法や位置に関わる値を読もうとすると、正しい答えを返すために溜まっている変更をその場で適用し、レイアウトを計算し直す必要があります。これを強制同期レイアウト(レイアウトスラッシング)と呼びます。
問題になるのは、書き込みと読み取りを交互に繰り返したときです。次のコードは、1要素ごとにレイアウト計算が走ります。
items.forEach((item) => {
// 読む → レイアウト強制
const width = parseFloat(getComputedStyle(item).width);
// 書く → 次のループでまたレイアウトが必要になる
item.style.width = `${width * 2}px`;
});
読み取りと書き込みをまとめて分ける
対処はシンプルで、全部読んでから、全部書くという順番にします。こうするとレイアウト計算は1回で済みます。
// 1. まとめて読む
const widths = items.map((item) => parseFloat(getComputedStyle(item).width));
// 2. まとめて書く
items.forEach((item, i) => {
item.style.width = `${widths[i] * 2}px`;
});
そもそも毎回呼ばない
全要素で共通の値(テーマカラーや基準のフォントサイズなど)は、ループの外で一度だけ取得して変数に入れておけば十分です。scroll や resize のような高頻度のイベントの中で呼ぶのも避け、値をキャッシュするか、変更があったときだけ取り直すようにします。
要素の表示サイズや位置を知りたいだけなら、getBoundingClientRect() のほうが目的に合っています。文字列ではなく数値が返るので parseFloat() も不要です。サイズの変化を監視したいなら ResizeObserver を使えば、毎フレーム測り直す必要がなくなります。
まとめ
element.style は style 属性を読み書きするためのもので、CSS ファイルの指定は見えません。実際に適用されているスタイルを知りたいときは window.getComputedStyle(element) を使います。返るのは解決済みの値なので、em や % は px に、色は rgb() 形式に揃えられ、常に文字列で返ります。
第2引数に '::before' を渡せば擬似要素のスタイルも読めますし、CSS カスタムプロパティは getPropertyValue('--name') に trim() を添えて取得します。一方で、display: none の要素では寸法が px に解決されないこと、返ってきたオブジェクトには書き込めないこと、ループの中で多用すると強制同期レイアウトで遅くなることは覚えておいてください。読み取りは getComputedStyle()、書き込みは element.style かクラスの付け替え、という住み分けを守れば安全に使えます。