新しい CSS 型オブジェクト モデルを使用する

Eric Bidelman 氏

要約

JavaScript で値を操作するための適切なオブジェクト ベースの API が CSS に追加されました。

el.attributeStyleMap.set('padding', CSS.px(42));
const padding = el.attributeStyleMap.get('padding');
console.log(padding.value, padding.unit); // 42, 'px'

文字列を連結し、微妙なバグを連結する時代は終わりました。

はじめに

以前の CSSOM

CSS には長年にわたってオブジェクト モデル(CSSOM)があります。実際、JavaScript で .style の読み取りや設定を行う際は、常にそれを使用しています。

// Element styles.
el.style.opacity = 0.3;
typeof el.style.opacity === 'string' // Ugh. A string!?

// Stylesheet rules.
document.styleSheets[0].cssRules[0].style.opacity = 0.3;

新しい CSS タイプ OM

Houdini の取り組みの一環として、新しい CSS Typed Object Model(型付き OM)は、タイプ、メソッド、適切なオブジェクト モデルを CSS 値に追加することで、この世界観を拡張します。値は文字列ではなく JavaScript オブジェクトとして公開されるため、CSS を効率的に(かつ実用的な)操作を行うことができます。

element.style を使用する代わりに、要素の新しい .attributeStyleMap プロパティとスタイルシート ルールの .styleMap プロパティを使用してスタイルにアクセスします。どちらも StylePropertyMap オブジェクトを返します。

// Element styles.
el.attributeStyleMap.set('opacity', 0.3);
typeof el.attributeStyleMap.get('opacity').value === 'number' // Yay, a number!

// Stylesheet rules.
const stylesheet = document.styleSheets[0];
stylesheet.cssRules[0].styleMap.set('background', 'blue');

StylePropertyMap は Map のようなオブジェクトであるため、通常のすべての疑い(get/set/keys/values/entries)をサポートし、柔軟に使用できます。

// All 3 of these are equivalent:
el.attributeStyleMap.set('opacity', 0.3);
el.attributeStyleMap.set('opacity', '0.3');
el.attributeStyleMap.set('opacity', CSS.number(0.3)); // see next section
// el.attributeStyleMap.get('opacity').value === 0.3

// StylePropertyMaps are iterable.
for (const [prop, val] of el.attributeStyleMap) {
  console.log(prop, val.value);
}
// → opacity, 0.3

el.attributeStyleMap.has('opacity') // true

el.attributeStyleMap.delete('opacity') // remove opacity.

el.attributeStyleMap.clear(); // remove all styles.

2 番目の例では、opacity は文字列('0.3')に設定されていますが、後でプロパティが読み取られると数値が戻されます。

利点

CSS Typed OM はどんな問題を解決しようとしているのでしょうか。上記の例(およびこの記事の残りの部分)を見ると、CSS Typed OM は古いオブジェクト モデルよりもはるかに詳細であると言えるかもしれません。そう思う!

Typed OM を取り消す前に、Typed OM がもたらす主な機能をいくつか検討してください。

  • バグの削減。たとえば、数値は文字列ではなく常に数値として返されます。

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • 算術演算と単位変換。絶対長の単位間の変換(例: px -> cm)を行い、基本的な計算を行います

  • 値の固定と丸め。型付きの OM は、値がプロパティの許容範囲内に収まるように、値を丸めたりクランプしたりします。

  • パフォーマンスの向上:ブラウザが行う文字列値のシリアル化とシリアル化解除の作業は少なくなります。現在、このエンジンは JS と C++ の CSS 値を同じように理解しています。Tab Akins は初期のパフォーマンス ベンチマークをいくつか示しています。これにより、Typed OM は古い CSSOM と文字列を使用した場合と比較して、1 秒あたりのオペレーション数が約 30% 高速になります。これは、requestionAnimationFrame() を使用した迅速な CSS アニメーションにとっては重要です。crbug.com/808933 は、Blink でさらなるパフォーマンス作業を追跡しています。

  • エラー処理。新しい解析メソッドにより、CSS にエラー処理が導入されます。

  • 「キャメルケース形式の CSS 名や文字列を使用すべきですか?」名前がキャメルケースか文字列か(たとえば、el.style.backgroundColorel.style['background-color'])のどちらであるかを推測する必要はありません。型付き OM の CSS プロパティ名は常に文字列であり、CSS で実際に記述した内容と一致するものです。

ブラウザ サポートと機能検出

型指定された OM は Chrome 66 で導入され、Firefox に実装されています。Edge にはサポートの兆候が示されていますが、プラットフォーム ダッシュボードにはまだ追加されていません。

特徴検出では、CSS.* 数値ファクトリのいずれかが定義されているかどうかを確認できます。

if (window.CSS && CSS.number) {
  // Supports CSS Typed OM.
}

API の基本

スタイルへのアクセス

値は CSS Typed OM の単位とは別のものです。スタイルを取得すると、valueunit を含む CSSUnitValue が返されます。

el.attributeStyleMap.set('margin-top', CSS.px(10));
// el.attributeStyleMap.set('margin-top', '10px'); // string arg also works.
el.attributeStyleMap.get('margin-top').value  // 10
el.attributeStyleMap.get('margin-top').unit // 'px'

// Use CSSKeyWorldValue for plain text values:
el.attributeStyleMap.set('display', new CSSKeywordValue('initial'));
el.attributeStyleMap.get('display').value // 'initial'
el.attributeStyleMap.get('display').unit // undefined

計算済みスタイル

計算済みスタイルが、window の API から HTMLElement の新しいメソッド computedStyleMap() に移動しました。

以前の CSSOM

el.style.opacity = 0.5;
window.getComputedStyle(el).opacity === "0.5" // Ugh, more strings!

新しいタイプ付き OM

el.attributeStyleMap.set('opacity', 0.5);
el.computedStyleMap().get('opacity').value // 0.5

値の固定 / 丸め

新しいオブジェクト モデルの優れた機能に、スタイルの計算値の自動クランプや丸めがあります。たとえば、opacity を許容範囲 [0, 1] 外の値に設定しようとしているとします。型指定された OM は、スタイルの計算時に値を 1 に固定します。

el.attributeStyleMap.set('opacity', 3);
el.attributeStyleMap.get('opacity').value === 3  // val not clamped.
el.computedStyleMap().get('opacity').value === 1 // computed style clamps value.

同様に、z-index:15.4 を設定すると、15 に丸められるため、値は整数のままになります。

el.attributeStyleMap.set('z-index', CSS.number(15.4));
el.attributeStyleMap.get('z-index').value  === 15.4 // val not rounded.
el.computedStyleMap().get('z-index').value === 15   // computed style is rounded.

CSS 数値

型付き OM では、数値は 2 種類の CSSNumericValue オブジェクトで表されます。

  1. CSSUnitValue - 単一のユニットタイプを含む値(例: "42px")。
  2. CSSMathValue - 数式など、複数の値/単位を含む値(例: "calc(56em + 10%)")。

単位値

単純な数値("50%")は、CSSUnitValue オブジェクトで表されます。これらのオブジェクトを直接作成(new CSSUnitValue(10, 'px'))することもできますが、ほとんどの場合は CSS.* ファクトリ メソッドを使用します。

const {value, unit} = CSS.number('10');
// value === 10, unit === 'number'

const {value, unit} = CSS.px(42);
// value === 42, unit === 'px'

const {value, unit} = CSS.vw('100');
// value === 100, unit === 'vw'

const {value, unit} = CSS.percent('10');
// value === 10, unit === 'percent'

const {value, unit} = CSS.deg(45);
// value === 45, unit === 'deg'

const {value, unit} = CSS.ms(300);
// value === 300, unit === 'ms'

CSS.* メソッドの全リストについては、仕様をご覧ください。

数学値

CSSMathValue オブジェクトは数式を表し、通常は複数の値/単位を含みます。一般的な例は CSS calc() 式の作成ですが、すべての CSS 関数に使用できるメソッドは calc()min()max() です。

new CSSMathSum(CSS.vw(100), CSS.px(-10)).toString(); // "calc(100vw + -10px)"

new CSSMathNegate(CSS.px(42)).toString() // "calc(-42px)"

new CSSMathInvert(CSS.s(10)).toString() // "calc(1 / 10s)"

new CSSMathProduct(CSS.deg(90), CSS.number(Math.PI/180)).toString();
// "calc(90deg * 0.0174533)"

new CSSMathMin(CSS.percent(80), CSS.px(12)).toString(); // "min(80%, 12px)"

new CSSMathMax(CSS.percent(80), CSS.px(12)).toString(); // "max(80%, 12px)"

ネストされた式

数学関数を使用してより複雑な値を生成すると、若干混乱が生じます。以下に例をいくつかご紹介します。インデントを増やして読みやすくしました

calc(1px - 2 * 3em) は次のように構築されます。

new CSSMathSum(
  CSS.px(1),
  new CSSMathNegate(
    new CSSMathProduct(2, CSS.em(3))
  )
);

calc(1px + 2px + 3px) は次のように構築されます。

new CSSMathSum(CSS.px(1), CSS.px(2), CSS.px(3));

calc(calc(1px + 2px) + 3px) は次のように構築されます。

new CSSMathSum(
  new CSSMathSum(CSS.px(1), CSS.px(2)),
  CSS.px(3)
);

算術演算

CSS Typed OM の便利な機能の一つは、CSSUnitValue オブジェクトに対して算術演算を実行できることです。

基本的なオペレーション

基本的なオペレーション(add/sub/mul/div/min/max)がサポートされています。

CSS.deg(45).mul(2) // {value: 90, unit: "deg"}

CSS.percent(50).max(CSS.vw(50)).toString() // "max(50%, 50vw)"

// Can Pass CSSUnitValue:
CSS.px(1).add(CSS.px(2)) // {value: 3, unit: "px"}

// multiple values:
CSS.s(1).sub(CSS.ms(200), CSS.ms(300)).toString() // "calc(1s + -200ms + -300ms)"

// or pass a `CSSMathSum`:
const sum = new CSSMathSum(CSS.percent(100), CSS.px(20)));
CSS.vw(100).add(sum).toString() // "calc(100vw + (100% + 20px))"

コンバージョン

絶対長さの単位は、他の単位長さに変換できます。

// Convert px to other absolute/physical lengths.
el.attributeStyleMap.set('width', '500px');
const width = el.attributeStyleMap.get('width');
width.to('mm'); // CSSUnitValue {value: 132.29166666666669, unit: "mm"}
width.to('cm'); // CSSUnitValue {value: 13.229166666666668, unit: "cm"}
width.to('in'); // CSSUnitValue {value: 5.208333333333333, unit: "in"}

CSS.deg(200).to('rad').value // 3.49066...
CSS.s(2).to('ms').value // 2000

平等

const width = CSS.px(200);
CSS.px(200).equals(width) // true

const rads = CSS.deg(180).to('rad');
CSS.deg(180).equals(rads.to('deg')) // true

CSS 変換値

CSS 変換は、CSSTransformValue を使用して作成され、変換値の配列(CSSRotateCSScaleCSSSkewCSSSkewXCSSSkewY など)を渡します。たとえば、次の CSS を再作成するとします。

transform: rotateZ(45deg) scale(0.5) translate3d(10px,10px,10px);

タイプ付き OM に翻訳:

const transform =  new CSSTransformValue([
  new CSSRotate(CSS.deg(45)),
  new CSSScale(CSS.number(0.5), CSS.number(0.5)),
  new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10))
]);

冗長な表現に加えて(大爆笑)、CSSTransformValue には便利な機能があります。2D 変換と 3D 変換を区別するブール値プロパティと、変換の DOMMatrix 表現を返す .toMatrix() メソッドがあります。

new CSSTranslate(CSS.px(10), CSS.px(10)).is2D // true
new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10)).is2D // false
new CSSTranslate(CSS.px(10), CSS.px(10)).toMatrix() // DOMMatrix

例: 立方体のアニメーション化

変換を使用する実践的な例を見てみましょう。JavaScript と CSS 変換を使って 立方体をアニメーションにします

const rotate = new CSSRotate(0, 0, 1, CSS.deg(0));
const transform = new CSSTransformValue([rotate]);

const box = document.querySelector('#box');
box.attributeStyleMap.set('transform', transform);

(function draw() {
  requestAnimationFrame(draw);
  transform[0].angle.value += 5; // Update the transform's angle.
  // rotate.angle.value += 5; // Or, update the CSSRotate object directly.
  box.attributeStyleMap.set('transform', transform); // commit it.
})();

次の点に注目してください。

  1. 数値は、数学を使用して角度を直接増分できることを意味します。
  2. アニメーションは、DOM に触れたり、すべてのフレームで値を読み取ったりするのではなく(box.style.transform=`rotate(0,0,1,${newAngle}deg)` がないなど)、基になる CSSTransformValue データ オブジェクトを更新してパフォーマンスを向上させます

デモ

ブラウザが Typed OM に対応している場合は、下に赤いキューブが表示されます。カーソルを合わせると、立方体が回転し始めます。アニメーションは CSS Typed OM を利用しています。🤘

CSS カスタム プロパティの値

CSS var() が型付き OM で CSSVariableReferenceValue オブジェクトになります。これらの値は、任意の型(px、%、em、 rgba() など)を取ることができるため、CSSUnparsedValue に解析されます。

const foo = new CSSVariableReferenceValue('--foo');
// foo.variable === '--foo'

// Fallback values:
const padding = new CSSVariableReferenceValue(
    '--default-padding', new CSSUnparsedValue(['8px']));
// padding.variable === '--default-padding'
// padding.fallback instanceof CSSUnparsedValue === true
// padding.fallback[0] === '8px'

カスタム プロパティの値を取得するには、次の作業を行う必要があります。

<style>
  body {
    --foo: 10px;
  }
</style>
<script>
  const styles = document.querySelector('style');
  const foo = styles.sheet.cssRules[0].styleMap.get('--foo').trim();
  console.log(CSSNumericValue.parse(foo).value); // 10
</script>

位置の値

スペースで区切られた x/y 位置を取る CSS プロパティ(object-position など)は、CSSPositionValue オブジェクトで表されます。

const position = new CSSPositionValue(CSS.px(5), CSS.px(10));
el.attributeStyleMap.set('object-position', position);

console.log(position.x.value, position.y.value);
// → 5, 10

値の解析

Typed OM は、ウェブ プラットフォームに解析メソッドを導入します。つまり、最終的に CSS 値をプログラムで解析してから使用できます。この新機能は、早期のバグや不正な形式の CSS を検出できる場合があることをご理解いただけます。

完全なスタイルを解析します。

const css = CSSStyleValue.parse(
    'transform', 'translate3d(10px,10px,0) scale(0.5)');
// → css instanceof CSSTransformValue === true
// → css.toString() === 'translate3d(10px, 10px, 0) scale(0.5)'

値を CSSUnitValue に解析します。

CSSNumericValue.parse('42.0px') // {value: 42, unit: 'px'}

// But it's easier to use the factory functions:
CSS.px(42.0) // '42px'

エラー処理

- CSS パーサーがこの transform 値で問題ないかどうかを確認します。

try {
  const css = CSSStyleValue.parse('transform', 'translate4d(bogus value)');
  // use css
} catch (err) {
  console.err(err);
}

まとめ

ついに CSS のオブジェクト モデルが最新になったことは喜ばしいことです。私には文字列を使うことに抵抗がありませんでした。CSS Typed OM API はやや複雑ですが、将来的にバグが減り、コードのパフォーマンスが向上する可能性があります。