CSS Typed OM: Use Typed Values Instead of CSS Strings
CSS values have always been strings in JavaScript. Reading element.style.width gives you "100px", a string you then have to parse, split, and convert yourself. Arithmetic on those strings doesn’t work at all: "100px" + "50px" is "100px50px", not "150px".
CSS Typed OM fixes this by giving you native JavaScript objects that represent CSS values. Instead of strings, you work with CSSUnitValue, CSSMathValue, and CSSTransformValue objects. The API ships in all modern browsers and covers every CSS property on every element.
The problem with string-based CSS
When you read a width value the old way, you get a string:
const width = element.style.width;
// → "100px"
Getting a string back means every numeric operation needs a manual parse step. Want to add 50px? You strip the unit, convert to a number, add, then glue the unit back on. Want to compare two widths? Same drill, twice. This is where Typed OM changes the experience.
// String-based approach: fragile
const width = parseInt(element.style.width, 10); // 100
const newWidth = width + 50;
element.style.width = newWidth + 'px'; // "150px"
This breaks the moment the unit changes or the value uses calc(). There is no type safety, no unit access, and no way to do math directly. Every measurement operation requires a parse step, every mutation requires string concatenation, and a single unit mismatch silently produces wrong results.
Reading inline styles with attributestylemap
Every element has an attributeStyleMap property that exposes its inline styles as a typed StylePropertyMap. This replaces the string-based element.style API.
Setting a typed value
const el = document.querySelector('#box');
el.attributeStyleMap.set('width', CSS.px(100));
el.attributeStyleMap.set('height', CSS.px(100));
el.attributeStyleMap.set('background', 'tomato');
CSS.px(), CSS.em(), CSS.deg(), and dozens more are factory methods on the global CSS object. They create CSSUnitValue instances so you never have to write new CSSUnitValue(100, 'px'). String values like 'tomato' are accepted as-is and stored as CSSKeywordValue objects, so you can mix typed and untyped values in the same style map.
Reading a typed value back
const w = el.attributeStyleMap.get('width');
// → CSSUnitValue { value: 100, unit: "px" }
console.log(w.value); // 100
console.log(w.unit); // "px"
console.log(w.toString()); // "100px"
The value is a proper JavaScript object. You access the number directly as .value and the unit as .unit. Calling .toString() gives you back the CSS string representation, so you can pass it to any API that expects a string.
Checking and deleting properties
el.attributeStyleMap.has('width'); // → true
el.attributeStyleMap.delete('width'); // removes the property
el.attributeStyleMap.has('width'); // → false
attributeStyleMap is a live map: changes you make through the API are reflected in the element’s inline styles immediately. The .has() and .delete() methods follow the same contract as JavaScript Map. Calling .toString() on a value gives you the CSS string representation for any context that expects a string. The map also supports iteration and size queries for bulk inspection of inline styles.
Other attributestylemap operations
// Check how many properties are set
el.attributeStyleMap.size; // → 2
// Iterate over all set properties
for (const [prop, val] of el.attributeStyleMap) {
console.log(prop, val.toString());
}
Iterating over attributeStyleMap yields only the inline styles set through the Typed OM API, not styles from stylesheets or inherited values. The .size property reflects the count of those entries.
Reading computed styles with computedstylemap()
The computedStyleMap() method returns a read-only StylePropertyMapReadOnly containing fully computed values after cascade, inheritance, and custom property resolution. Unlike attributeStyleMap, calling it always returns a fresh snapshot.
const map = element.computedStyleMap();
const fontSize = map.get('font-size');
// → CSSUnitValue { value: 16, unit: "px" } (resolved from em/rem if used)
This is useful when you need the final pixel value regardless of what unit the stylesheet used. You access the value the same way as attributeStyleMap, but the returned map is read-only. Calling .set() or .delete() throws a TypeError because the snapshot is immutable.
Converting between units
CSSUnitValue objects expose a .to(unit) method for converting between compatible units:
const map = element.computedStyleMap();
const fontSize = map.get('font-size');
// → CSSUnitValue { value: 2, unit: "em" }
const pxSize = fontSize.to('px');
// → CSSUnitValue { value: 32, unit: "px" } (assumes 16px root)
Not all conversions are valid. You cannot convert px to deg, for example. The method throws a TypeError for incompatible unit pairs.
Arithmetic with cssmathvalue
CSSMathValue represents CSS calc() expressions. Its subclasses correspond to each operation:
CSSMathSum:calc(a + b)CSSMathProduct:calc(a * b)CSSMathMin:calc(min(a, b))CSSMathMax:calc(max(a, b))CSSMathNegate:calc(-a)
Adding two length values
const a = new CSSUnitValue(10, 'px');
const b = new CSSUnitValue(5, 'px');
const sum = new CSSMathSum(a, b);
console.log(sum.toString()); // "calc(10px + 5px)"
CSSMathSum combines values with addition. The operands can be any CSSNumericValue, including other CSSMathValue instances, so expressions can nest to model any calc() expression that CSS supports. You can add lengths to other lengths, angles to angles, or mix compatible units like px and rem in the same sum.
Multiplying a value
const base = new CSSUnitValue(3, 'em');
const scaled = new CSSMathProduct(base, CSS.number(2));
console.log(scaled.toString()); // "calc(3em * 2)"
// Use it directly in the style map
element.attributeStyleMap.set('font-size', scaled);
CSSMathProduct takes a CSSNumericValue and multiplies it by a CSSNumberValue. The result is a calc() expression that the browser evaluates when the style is applied, so it stays responsive to layout context. This pattern is useful for scaling font sizes or spacing values relative to a base unit without hardcoding the computed result.
Negating a value
const offset = new CSSUnitValue(20, 'px');
const negated = new CSSMathNegate(offset);
element.attributeStyleMap.set('margin-left', negated);
// → margin-left: calc(-20px)
CSSMathNegate wraps a single value with a negative sign. It is less common than the other operations but completes the set of arithmetic primitives so that any calc() expression can be represented as a typed object tree. The negated value retains its unit and can be used anywhere a normal CSSUnitValue would go.
Chaining math operations
const result = new CSSMathSum(
new CSSUnitValue(100, 'vw'),
new CSSMathProduct(
new CSSUnitValue(-2, 'rem'),
CSS.number(10)
)
);
// → calc(100vw + -2rem * 10)
The nesting maps directly to how calc() works in CSS. Each nested CSSMathValue becomes a parenthesized sub-expression, so the typed representation mirrors the CSS syntax one-to-one. You can build arbitrarily complex expressions by composing the arithmetic primitives.
Composing transforms with csstransformvalue
CSS transform properties accept multiple functions in sequence. CSSTransformValue holds an array of CSSTransformComponent objects, one per transform function.
Creating a transform value
const transform = new CSSTransformValue([
new CSSTranslate(CSS.px(50), CSS.px(100)),
new CSSRotate(CSS.deg(45)),
new CSSScale(CSS.number(1.5), CSS.number(1.5))
]);
element.attributeStyleMap.set('transform', transform);
CSSTransformValue accepts an array of CSSTransformComponent objects. The order matters: each component is applied in sequence, just as in CSS transform declarations where translate() before rotate() produces a different result than rotate() before translate().
Available transform components
| Class | CSS equivalent |
|---|---|
CSSTranslate(x, y, z) | translate(x, y, z) |
CSSRotate(angle) | rotate(angle) |
CSSScale(x, y, z) | scale(x, y, z) |
CSSSkew(ax, ay) | skew(ax, ay) |
CSSPerspective(length) | perspective(length) |
Each component class accepts typed CSS values as constructor arguments, so you build transforms with full unit safety: CSS.deg(45) instead of the string "45deg", CSS.px(100) instead of "100px". The browser resolves the final matrix from these typed components at render time, and the individual components remain inspectable through the API. This means you can read back a transform, modify one axis of a scale or one angle of a rotation, and apply the updated transform without rebuilding the entire string.
Reading a transform back
const current = element.attributeStyleMap.get('transform');
// → CSSTransformValue [ CSSTranslate(...), CSSRotate(...), CSSScale(...) ]
Reading a transform back from the style map returns the same typed objects you set. The components preserve their values and types, so you can modify individual parts without rebuilding the whole string. For example, you can read a rotate component, change only the angle, and apply the updated transform without touching the translate or scale values.
Inverting a transform
const original = element.attributeStyleMap.get('transform');
const inverted = original.toMatrix().inverse;
CSSTransformValue composes cleanly. You can extract the matrix representation and derive from it. Calling .toMatrix() computes the combined transformation matrix, which is useful for hit-testing, coordinate mapping, and physics calculations where you need the final 2D or 3D position. The .inverse property on the matrix gives you the reverse transformation without manual matrix math, so you can map coordinates back through a transformed element. This chain of operations reads naturally: get the transform, convert to a matrix, and work with the result as a mathematical object.
Typed VS string: a side-by-side comparison
// === OLD: string-based ===
// Read
const width = parseInt(getComputedStyle(el).width, 10); // manual parse
// Write
el.style.width = (width + 50) + 'px'; // string concat everywhere
// Math
parseInt(el.style.width, 10) + parseInt(otherEl.style.width, 10); // error-prone
// Transform
el.style.transform = `rotate(${angle}deg) scale(${scale})`; // template literal every time
// === NEW: typed om ===
// Read
const width = el.computedStyleMap().get('width'); // CSSUnitValue, no parsing
// Write
el.attributeStyleMap.set('width', CSS.px(width.value + 50)); // typed arithmetic
// Math
new CSSMathSum(width, otherWidth).toString(); // "calc(Xpx + Ypx)"
// Transform
el.attributeStyleMap.set('transform',
new CSSTransformValue([CSSRotate(CSS.deg(angle)), CSSScale(CSS.number(scale))])
);
Use case: responsive layout calculations
When building layout code that reads dimensions from the DOM and derives new values, Typed OM removes the parsing overhead entirely. For other ways to interact with the DOM programmatically, see the Custom Elements guide. Every value you read is already a number with its unit attached, so layout math becomes straightforward arithmetic.
function getElementSize(el) {
const map = el.computedStyleMap();
return {
width: map.get('width').to('px').value,
height: map.get('height').to('px').value
};
}
function setScaledSize(el, scale) {
const { width, height } = getElementSize(el);
el.attributeStyleMap.set('width', CSS.px(width * scale));
el.attributeStyleMap.set('height', CSS.px(height * scale));
}
Every value you read is already a number. Every value you write is already typed. No parseInt calls, no template literals, no regex.
Use case: animations with the web animations API
The Web Animations API pairs naturally with Typed OM. Keyframes use typed values directly. No string interpolation:
const el = document.querySelector('.slide-in');
const startTransform = new CSSTransformValue([
new CSSTranslate(CSS.px(-200), CSS.px(0))
]);
const endTransform = new CSSTransformValue([
new CSSTranslate(CSS.px(0), CSS.px(0))
]);
const animation = el.animate([
{ transform: startTransform, opacity: CSS.number(0) },
{ transform: endTransform, opacity: CSS.number(1) }
], {
duration: 600,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
fill: 'forwards'
});
Because animate() accepts CSSStyleValue objects in keyframes, you compose transforms and transition opacity values without ever touching a string. The keyframe array can mix typed values and plain numbers, and the browser handles interpolation between matching types automatically.
Working with custom properties (CSS variables)
Custom properties resolve through computedStyleMap() the same as any other property:
const map = element.computedStyleMap();
const hue = map.get('--brand-hue');
if (hue instanceof CSSUnitValue) {
console.log(`Brand hue: ${hue.value}deg`);
}
// Set a custom property with a typed value
element.attributeStyleMap.set('--rotation', CSS.deg(90));
console.log(element.attributeStyleMap.get('--rotation').toString()); // "90deg"
Custom properties store their resolved type, so checking instanceof CSSUnitValue before accessing .value is good practice.
Summary
CSS Typed OM replaces string manipulation with typed JavaScript objects:
attributeStyleMap: read and write inline styles on an element as typed valuescomputedStyleMap(): read the fully computed, resolved value of any CSS propertyCSSUnitValue: a number with an associated unit (100px,1.5em,45deg)CSSMathValue: arithmetic (calc()) as nested typed objectsCSSTransformValue: composed transform functions as typed objects
The global CSS object provides factory methods (CSS.px(), CSS.deg(), CSS.number()) for creating values without explicit constructors. Values convert back to strings with .toString(), so you can pass typed values to any API that expects a CSS string. Every factory method returns a CSSUnitValue with the correct unit already attached, eliminating typos in unit strings. The factory names match CSS unit keywords exactly, so CSS.px() maps to 'px', CSS.rem() to 'rem', and CSS.deg() to 'deg'.
Browser support covers Chrome 66+, Firefox 85+, Safari 16.4+, and Edge 79+. Older browsers degrade values to strings gracefully. You can feature-detect with a simple check:
if (element.attributeStyleMap) {
// Use Typed OM
} else {
// Fall back to string-based style API
}
Reading values for layout math
When a measurement comes from the DOM, keeping it typed saves repeated parsing. You can move from computed width to a scaled width without converting strings back and forth, which reduces little bugs around units. That is especially useful when a component needs to respond to resize changes or theme changes in a predictable way.
Using typed transforms
Transforms are a good fit for typed values because the browser already treats them as structured operations. A rotate, translate, or scale step can live as a real object instead of a string template. That makes each change easier to inspect and combine, especially when animations or gesture handlers update the same element over time.
Custom properties and tokens
Typed custom properties are helpful when your CSS variables carry numbers, lengths, or angles rather than raw text. Design tokens become easier to inspect, compare, and update because the unit stays attached to the value. That reduces the chance that a later calculation accidentally assumes the wrong unit or format.
When strings are still fine
Typed OM does not replace every style task. Small one-off style changes, static CSS classes, and browser support checks can still use plain strings without friction. The value of the API shows up when you read and write the same kinds of measurements repeatedly. Use the typed path where it improves clarity, and keep simpler style code where it already works.
Typed values make refactors easier
When you move from strings to typed values, changing units becomes less brittle. A length stays a length, an angle stays an angle, and the browser does the unit handling for you. That means later refactors can adjust layout or animation math without rewriting a lot of string parsing.
Feature detection still matters
Typed OM is useful, but feature detection keeps the page safe across browsers. If the typed path is not available, fall back to the regular style APIs and keep the page functional. That way the improved code path stays an improvement rather than a requirement.
Keep numeric values numeric
When a style value is really a measurement, treat it as a measurement all the way through. That keeps your calculations honest and makes it easier to compare values across components. The typed API is especially useful when a layout depends on several values that all need to stay in the same unit family.
Use typed OM where math repeats
The biggest payoff comes when the same style math happens again and again. If a component keeps reading, adjusting, and writing the same property, typed values remove a lot of manual parsing. That can make sizing logic easier to trust and much easier to revisit later.