How to Safely Handle CSS Values as Numbers in JavaScript — A Practical Guide to CSS Typed OM and attributeStyleMap
Have you ever read a value with getComputedStyle(el).width inside animation logic, added a number to it, and ended up with a bizarre string like "200px5" in the console? I used to just wrap it in parseFloat() and move on, but once that pattern spread throughout a team codebase, problems eventually surfaced. During a design system migration, someone merged without parseFloat, and it turned into a bug where a component's dimensions were off by a fraction of a pixel in certain layouts. It took a long time to track down the root cause because QA couldn't easily reproduce it.
The root of this problem lies in the legacy CSSOM's design: it returns all CSS values as strings. The CSS engine manages numbers and units internally as separate structs, but serializes them into strings like "200px" when handing them to JavaScript. Even with TypeScript, the return type of getComputedStyle is string, so static analysis alone can't catch this mistake.
With CSS Typed OM, CSS values are returned as typed objects with the number and unit separated, enabling safe numeric operations without parseFloat. This article covers everything from how element.attributeStyleMap and CSSUnitValue work, to real-world animation patterns, TypeScript usage, and current browser support — at a level you can apply directly in production. If you know CSS and JavaScript but have never heard of CSS Typed OM, this is the right starting point.
Core Concepts
Legacy CSSOM vs. CSS Typed OM
Understanding the old approach first makes it clear why Typed OM is needed.
const el = document.getElementById('box');
// Legacy CSSOM — returns a string
const width = getComputedStyle(el).width; // "200px" (string)
// Parsing is required for numeric operations
const newWidth = parseFloat(width) + 50; // 200 → 250
el.style.width = newWidth + 'px'; // convert back to string
// An easy mistake to make
const bad = width + 50; // "200px50" — string concatenation!CSS Typed OM removes this layer. Values are returned as typed JavaScript objects rather than strings.
// CSS Typed OM — returns typed objects
el.attributeStyleMap.set('width', CSS.px(200));
const typed = el.attributeStyleMap.get('width');
typed.value; // 200 (number)
typed.unit; // "px" (string)
// Direct arithmetic without parseFloat
el.attributeStyleMap.set('width', CSS.px(typed.value + 50)); // 250pxTwo Key Interfaces
CSS Houdini is a set of low-level APIs that expose the browser rendering engine's internals to JavaScript. It includes CSS Typed OM, Paint Worklet, the Properties and Values API, and more, with specifications managed by the W3C.
There are two interfaces you'll use most often in CSS Typed OM.
| Interface | Role | Legacy Equivalent |
|---|---|---|
element.attributeStyleMap |
Read/write inline styles | element.style |
element.computedStyleMap() |
Read computed styles | getComputedStyle(element) |
I used to confuse these two myself, but here's a simple way to remember: "styles you set directly" use attributeStyleMap, and "styles the browser has finally computed" use computedStyleMap().
Two Forms of Value Representation
In Typed OM, CSS values are represented by two main types.
CSSUnitValue: A value with a single unit.CSS.px(42)→{ value: 42, unit: 'px' }CSSMathValue: A value representing a complex expression likecalc(56em + 10%)
CSSMathValue is returned when an expression like calc() or min() is involved, and this type has no .value property. Accessing .value directly on a value read from computedStyleMap() can cause a runtime error — this is covered again in the "Common Mistakes" section below.
Use CSS factory methods to create typed values.
CSS.px(42) // CSSUnitValue { value: 42, unit: 'px' }
CSS.em(1.5) // CSSUnitValue { value: 1.5, unit: 'em' }
CSS.percent(50) // CSSUnitValue { value: 50, unit: 'percent' }
CSS.deg(180) // CSSUnitValue { value: 180, unit: 'deg' }
CSS.number(0.5) // CSSUnitValue { value: 0.5, unit: 'number' }
CSS.ms(300) // CSSUnitValue { value: 300, unit: 'ms' }
StylePropertyMapis the object type returned byattributeStyleMap. It provides the same interface asMap(get,set,has,delete,keys,entries). I started using it just like aMapfrom the beginning, and since it's almost identical, I got comfortable with it quickly.
Practical Application
Example 1: Reading Inline Styles and Adding to Them
This is the most fundamental pattern. The scenario is reading the current margin value of an element and adding to it — just mastering this is enough to escape parseFloat hell.
const el = document.querySelector('.card');
// Set an initial value
el.attributeStyleMap.set('margin-top', CSS.px(10));
// Read → compute → write
// Note: since set() was called just before, get() here will not return null.
// Calling get() on an element without an inline style may return null.
const current = el.attributeStyleMap.get('margin-top');
console.log(current.value); // 10 (number)
console.log(current.unit); // "px"
el.attributeStyleMap.set('margin-top', CSS.px(current.value + 5));
// margin-top is safely set to 15px.In a TypeScript strict environment, since the return type of get() is CSSStyleValue | null, handling it this way is safe.
const el = document.querySelector('.card')!;
el.attributeStyleMap.set('margin-top', CSS.px(10));
const current = el.attributeStyleMap.get('margin-top');
if (current instanceof CSSUnitValue) {
// Here, current.value is narrowed to number.
el.attributeStyleMap.set('margin-top', CSS.px(current.value + 5));
}A single type guard handles both the null check and type narrowing, which is quite clean.
Example 2: Precise Calculations in a requestAnimationFrame Animation
Parsing strings every frame inside an animation loop is unnecessary overhead. With Typed OM, you pass objects directly to the CSS engine without parsing, which also makes the code much cleaner.
const el = document.querySelector('.moving-box');
let x = 0;
function animate() {
x += 2;
// CSSTranslate is a browser global. No separate import is needed.
el.attributeStyleMap.set(
'transform',
new CSSTranslate(CSS.px(x), CSS.px(0))
);
if (x < 500) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);The difference is clear when placed side by side with the old approach.
// Old approach — string creation + browser parsing every frame
el.style.transform = `translateX(${x}px)`;
// Typed OM — objects passed directly, no parsing
el.attributeStyleMap.set('transform', new CSSTranslate(CSS.px(x), CSS.px(0)));Example 3: Reading Computed Styles with computedStyleMap()
This is the Typed OM version of getComputedStyle(). It lets you read the final computed values determined by CSS classes or inheritance in a typed form.
const el = document.querySelector('.text');
const styleMap = el.computedStyleMap();
const fontSize = styleMap.get('font-size');
// CSSUnitValue { value: 16, unit: 'px' }
const lineHeight = styleMap.get('line-height');
// CSSUnitValue { value: 24, unit: 'px' }
// Direct arithmetic without parsing
const ratio = lineHeight.value / fontSize.value; // 1.5 (number)
console.log(`Line height ratio: ${ratio}`);Code that previously required something like parseFloat(getComputedStyle(el).fontSize) becomes clean and straightforward.
Example 4: Integration with CSS Custom Properties
Used together with the CSS Properties and Values API, you can define typed custom properties and safely read and write them with Typed OM. This is a useful pattern for theme systems or managing animation variables.
// Note: CSS.registerProperty() is only supported in Chrome/Edge.
// Firefox does not support CSS Typed OM or this API.
CSS.registerProperty({
name: '--rotation-angle',
syntax: '<angle>',
inherits: false,
initialValue: '0deg'
});
const el = document.querySelector('.spinner');
el.attributeStyleMap.set('--rotation-angle', CSS.deg(45));
const angle = el.attributeStyleMap.get('--rotation-angle');
console.log(angle.value); // 45 (number)
console.log(angle.unit); // "deg"
// Increment by 30 degrees
el.attributeStyleMap.set('--rotation-angle', CSS.deg(angle.value + 30));Since CSS.registerProperty() and CSS Typed OM share the same unsupported range in Firefox, this pattern particularly shines in Chrome/Edge-only environments such as internal dashboards, Chrome extensions, and Electron apps.
Pros and Cons
Advantages
| Item | Details |
|---|---|
| Type safety | Numeric values are always returned as number, structurally preventing string concatenation bugs like '10' + 5 = '105' |
| No parsing overhead | Since JS objects are passed directly to the CSS engine, there is no string serialization/deserialization cost |
| Unit-aware arithmetic | Values and units are separated, enabling conversions between absolute units and basic arithmetic — useful in tools like print layout utilities that need px → cm conversion |
| Value validity guaranteed | Values outside the allowed range are automatically clamped at the CSS engine level |
| TypeScript-friendly | Values can be narrowed to concrete types like CSSUnitValue and CSSMathValue, improving static analysis quality |
Honestly, the most tangible benefit in production is type safety. As long as getComputedStyle returns string, there's no way to statically catch a missed parseFloat even with TypeScript. That one-line structural guarantee makes a bigger difference than you might expect.
Drawbacks and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Incomplete browser support | Firefox has not yet implemented CSS Typed OM (tracked at Bugzilla #1278697) | Apply the csstools/css-typed-om polyfill or use a progressive enhancement pattern |
| Performance gains are conditional | In theory there is no parsing cost, but in current implementations of Chrome and Safari, internal conversion costs may still occur, so the benefit is not always realized | Benchmark directly before applying in performance-critical paths |
| API learning curve | Requires learning new interfaces and factory methods compared to element.style |
Adaptable quickly with MDN guides and the examples in this article |
| Polyfill limitations | csstools/css-typed-om does not fully implement the entire specification |
Verify in advance that the features you use are supported by the polyfill |
Progressive enhancement pattern: Detect feature support with something like
typeof CSS !== 'undefined' && CSS.px, use Typed OM if available, and fall back to the legacy CSSOM in unsupported environments for a safe rollout.
The Most Common Mistakes in Practice
-
Confusing
attributeStyleMapandcomputedStyleMap()— UseattributeStyleMap.set()when writing inline styles, andcomputedStyleMap().get()when reading the final computed style. BecauseattributeStyleMap.get()only returns values directly set as inline styles, it may returnnullfor styles applied via CSS classes or inheritance. -
Directly accessing
.valueon aCSSMathValueresult —computedStyleMap()does not always returnCSSUnitValue. When an expression likecalc()ormin()is involved, it returnsCSSMathValue, which has no.valueproperty. It is recommended to verify the type withinstanceof CSSUnitValuebefore accessing it. -
Applying to production without checking Firefox support — CSS Typed OM currently does not work in Firefox. Using it without a polyfill or feature detection will cause errors for all Firefox users. It is safer to first check for support with
typeof CSS !== 'undefined' && CSS.px.
Closing Thoughts
After reading this article, whenever you see the pattern parseFloat(getComputedStyle(...).someProperty) in a codebase, you'll recognize it as a spot that can be replaced with Typed OM. CSS Typed OM marks a turning point from "handling CSS with strings" to "handling CSS with typed objects," enabling structurally safe style manipulation code without string parsing.
The practical constraint of incomplete Firefox support remains, but in Chrome/Edge-only environments or alongside a polyfill, it is well worth adopting today.
Three steps to get started right now:
-
In the Chrome DevTools console, type
document.body.attributeStyleMap.set('background', new CSSKeywordValue('red'))to immediately confirm that Typed OM works in practice. -
Search your existing project for the
parseFloat(getComputedStyle(pattern to identify migration candidates. You can detect support withtypeof CSS !== 'undefined' && CSS.pxand perform a gradual replacement. -
If you need Firefox support, try installing the
csstools/css-typed-ompolyfill. After runningpnpm add @csstools/css-typed-omand importing it at your entry point, you can use the same API in unsupported environments as well.
Next article: CSS Paint API — drawing custom background images from JavaScript with CSS Houdini's Paint Worklet — where
registerPaint()meets the Canvas API
References
- CSS Typed Object Model API | MDN Web Docs
- Using the CSS Typed Object Model | MDN Guide
- Working with the new CSS Typed Object Model | Chrome for Developers
- CSS Typed OM Level 1 | W3C Official Specification
- csstools/css-typed-om | GitHub (polyfill)
- The Typed Object Model | CSS-Tricks
- A Practical Overview Of CSS Houdini | Smashing Magazine
- Firefox CSS Typed OM implementation tracking | Bugzilla #1278697