What is CSS Houdini's Typed OM?
CSS Typed Object Model (Typed OM) is a foundational part of the CSS Houdini suite of APIs. It exposes CSS values as typed JavaScript objects rather than the string-based representations we've been limited to for decades. Instead of parsing strings like "10px" or "rotate(45deg)", developers can now interact with structured, strongly-typed representations of those values—objects that understand their own units, can perform mathematical operations, and expose their internal structure directly.
At its core, Typed OM replaces the string-heavy interface of element.style with a rich object hierarchy. A CSS value like width: calc(50% + 20px) becomes a tree of CSSMathSum, CSSMathProduct, and CSSUnitValue objects that you can traverse, modify, and recompute programmatically without ever touching a fragile string parser.
The Object Hierarchy
Typed OM organizes CSS values into a class hierarchy rooted at CSSStyleValue. The key subclasses you'll work with include:
- CSSUnitValue — represents a single numeric value with a unit (e.g.,
10px,2em,50deg) - CSSMathValue — represents mathematical expressions like
calc(), with subclassesCSSMathSum,CSSMathProduct,CSSMathNegate,CSSMathInvert,CSSMathMin,CSSMathMax, andCSSMathClamp - CSSTransformValue — represents a list of transform functions like
translate(),rotate(),scale() - CSSUnparsedValue — a fallback for values that the browser cannot yet type, preserving the raw string and variable references
- CSSKeywordValue — represents keywords like
auto,none,block
Why Typed OM Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →For years, JavaScript manipulation of CSS meant string surgery. Consider this typical pattern:
// The old way: string parsing nightmare
const width = getComputedStyle(el).width; // "320px"
const numericWidth = parseFloat(width); // 320 — but what if it was "auto"?
el.style.width = (numericWidth + 20) + 'px'; // String concatenation, unit guessing
This approach has fundamental problems:
- Error-prone parsing — you must manually parse units, handle edge cases like
calc(), and reconstruct strings correctly - Performance overhead — every read-modify-write cycle requires string serialization and deserialization, often triggering layout recalculation at each step
- Limited introspection — you cannot easily inspect the internal structure of a
calc()expression or a list of transforms without writing your own parser - Unit ambiguity — a raw number loses its unit context, making it easy to accidentally mix pixels with percentages
Typed OM solves these problems by providing:
- Type safety — values carry their units and types through every operation
- Math capabilities — you can add, subtract, multiply, and divide typed values directly, with automatic unit conversion where supported
- Structural access — traverse and modify
calc()expression trees and transform lists without string parsing - Performance gains — by skipping the serialization/deserialization cycle, the browser can apply changes more efficiently
Getting Started with Typed OM
Accessing Typed OM Values
The entry point for reading typed values is the computedStyleMap() method on elements. This returns a StylePropertyMapReadOnly object keyed by CSS properties. For writable access, use element.attributeStyleMap for inline styles.
// Read computed styles as typed objects
const styleMap = element.computedStyleMap();
// Get the typed value for 'width'
const widthValue = styleMap.get('width');
console.log(widthValue); // CSSUnitValue { value: 320, unit: 'px' }
// Access numeric value and unit directly
if (widthValue instanceof CSSUnitValue) {
console.log(`Value: ${widthValue.value}, Unit: ${widthValue.units}`);
}
// For inline styles, use attributeStyleMap (read/write)
const inlineMap = element.attributeStyleMap;
const inlineWidth = inlineMap.get('width');
Working with CSSUnitValue
CSSUnitValue is the most common typed value. It represents a number-unit pair and can be constructed manually for setting styles.
// Creating CSSUnitValue instances
const tenPixels = new CSSUnitValue(10, 'px');
const fiftyPercent = new CSSUnitValue(50, 'percent');
const twoEms = new CSSUnitValue(2, 'em');
const rotation = new CSSUnitValue(45, 'deg');
const milliseconds = new CSSUnitValue(300, 'ms');
// Setting inline styles via Typed OM
element.attributeStyleMap.set('width', tenPixels);
element.attributeStyleMap.set('margin-left', twoEms);
// Checking types
console.log(tenPixels instanceof CSSUnitValue); // true
console.log(tenPixels.unit); // 'px' — note: singular form
console.log(tenPixels.value); // 10
Important detail: unit names in Typed OM use their singular form. 'px' not 'pxs', 'em' not 'ems', 'percent' not '%'. This is different from CSS string syntax.
Working with CSSMathValue and Calc Expressions
When a CSS value uses calc(), min(), max(), or clamp(), the typed OM represents it as a tree of CSSMathValue objects. You can traverse this tree, extract components, and even build new expressions programmatically.
// Suppose an element has: width: calc(100% - 40px)
const styleMap = element.computedStyleMap();
const widthValue = styleMap.get('width');
console.log(widthValue instanceof CSSMathSum); // true
// Inspect the expression tree
// CSSMathSum has an .operator and .values getter
console.log(widthValue.operator); // 'sum' (could also be 'negate', 'product', 'invert', 'min', 'max')
console.log(widthValue.values.length); // 2
// Access individual terms
const term0 = widthValue.values[0]; // CSSMathProduct or CSSUnitValue for 100%
const term1 = widthValue.values[1]; // CSSMathNegate wrapping 40px
// CSSMathSum automatically handles subtraction by negating the subtracted term
// calc(100% - 40px) becomes sum(100%, negate(40px))
console.log(term1 instanceof CSSMathNegate); // true
console.log(term1.value); // CSSUnitValue { value: 40, unit: 'px' }
You can construct CSSMathValue instances manually to create complex expressions:
// Building: calc((100% - 20px) * 1.5)
const hundredPercent = new CSSUnitValue(100, 'percent');
const twentyPx = new CSSUnitValue(20, 'px');
const multiplier = new CSSUnitValue(1.5, 'number');
// First create the sum: 100% + negate(20px) → 100% - 20px
const negatedTwenty = new CSSMathNegate(twentyPx);
const sum = new CSSMathSum(hundredPercent, negatedTwenty);
// Then wrap in product: sum * 1.5
const product = new CSSMathProduct(sum, multiplier);
// Apply it
element.attributeStyleMap.set('width', product);
Here's a more practical example building a responsive sizing expression:
// Build clamp(min-width, preferred, max-width)
const minWidth = new CSSUnitValue(200, 'px');
const preferred = new CSSUnitValue(50, 'percent');
const maxWidth = new CSSUnitValue(600, 'px');
const clampedWidth = new CSSMathClamp(minWidth, preferred, maxWidth);
element.attributeStyleMap.set('width', clampedWidth);
// This produces: width: clamp(200px, 50%, 600px)
Working with CSS Transform Values
Transforms are one of the most powerful applications of Typed OM. Instead of parsing a string like "translate(100px, 50px) rotate(45deg) scale(1.2)", you get a structured list of CSSTransformComponent objects.
// Read a transform value
const styleMap = element.computedStyleMap();
const transformValue = styleMap.get('transform');
// transformValue is a CSSTransformValue containing an array of components
console.log(transformValue instanceof CSSTransformValue); // true
console.log(transformValue.is2D); // true or false depending on content
for (const component of transformValue) {
console.log(component.constructor.name);
if (component instanceof CSSTranslate) {
console.log(`Translate X: ${component.x.value}${component.x.units}`);
console.log(`Translate Y: ${component.y.value}${component.y.units}`);
}
if (component instanceof CSSRotate) {
console.log(`Angle: ${component.angle.value}${component.angle.units}`);
}
if (component instanceof CSSScale) {
console.log(`Scale X: ${component.x.value}, Y: ${component.y.value}`);
}
}
Constructing transforms programmatically is equally straightforward:
// Build a composite transform
const translate = new CSSTranslate(
new CSSUnitValue(100, 'px'),
new CSSUnitValue(50, 'px')
);
const rotate = new CSSRotate({
angle: new CSSUnitValue(45, 'deg'),
// Optional: specify rotation axis for 3D
});
const scale = new CSSScale(
new CSSUnitValue(1.2, 'number'),
new CSSUnitValue(1.2, 'number')
);
// Combine into a single transform value
const compositeTransform = new CSSTransformValue([translate, rotate, scale]);
element.attributeStyleMap.set('transform', compositeTransform);
Setting Typed OM Values
Setting values via attributeStyleMap accepts both typed objects and raw strings (with automatic conversion), but using typed objects gives you the full benefits:
// All of these work
element.attributeStyleMap.set('opacity', new CSSUnitValue(0.5, 'number'));
element.attributeStyleMap.set('display', new CSSKeywordValue('flex'));
element.attributeStyleMap.set('width', new CSSUnitValue(100, 'px'));
// You can also pass strings — they'll be parsed into typed values
element.attributeStyleMap.set('width', '200px');
// After setting, reading back gives a typed value
console.log(element.attributeStyleMap.get('width')); // CSSUnitValue
Working with Custom Properties
Typed OM also supports CSS custom properties (variables). When a custom property is defined with a @property rule including a syntax descriptor, the browser can type its value:
// First, register the custom property with a type (via CSS or CSS.registerProperty)
// CSS:
// @property --my-angle {
// syntax: '';
// inherits: false;
// initial-value: 0deg;
// }
// Then use it in Typed OM
element.attributeStyleMap.set('--my-angle', new CSSUnitValue(45, 'deg'));
// Reading back preserves the type
const angleValue = element.computedStyleMap().get('--my-angle');
console.log(angleValue instanceof CSSUnitValue); // true
console.log(angleValue.value); // 45
console.log(angleValue.units); // 'deg'
For unregistered custom properties, values are wrapped in CSSUnparsedValue, which preserves the raw tokens:
// Unregistered custom property
element.attributeStyleMap.set('--my-color', 'red');
const value = element.computedStyleMap().get('--my-color');
console.log(value instanceof CSSUnparsedValue); // true
// CSSUnparsedValue contains an array of string or variable-reference tokens
for (const item of value) {
if (typeof item === 'string') {
console.log(`String token: ${item}`);
} else {
// item is a VariableReferenceValue for var() references
console.log(`Var reference: ${item.variable}, fallback: ${item.fallback}`);
}
}
Practical Examples
Building a Dynamic Gradient Generator
Let's build a complete example that dynamically adjusts a CSS gradient based on user input, using Typed OM to manipulate values without string parsing.
<div id="gradient-box" style="
width: 400px;
height: 300px;
background-image: linear-gradient(45deg, red 0%, blue 100%);
"></div>
<label>Angle: <input type="range" id="angle-slider" min="0" max="360" value="45"></label>
<label>Start position: <input type="range" id="start-pos" min="0" max="100" value="0"></label>
<label>End position: <input type="range" id="end-pos" min="0" max="100" value="100"></label>
<script>
const box = document.getElementById('gradient-box');
const angleSlider = document.getElementById('angle-slider');
const startPosSlider = document.getElementById('start-pos');
const endPosSlider = document.getElementById('end-pos');
function updateGradient() {
const angle = new CSSUnitValue(angleSlider.value, 'deg');
const startPos = new CSSUnitValue(startPosSlider.value, 'percent');
const endPos = new CSSUnitValue(endPosSlider.value, 'percent');
// Build the gradient value using Typed OM
// linear-gradient(angle, color1 position1, color2 position2)
// We construct a CSSLinearGradient value
const gradient = new CSSLinearGradient(
angle, // gradient angle
new CSSKeywordValue('red'), // first color stop color
startPos, // first color stop position
new CSSKeywordValue('blue'), // second color stop color
endPos // second color stop position
);
box.attributeStyleMap.set('background-image', gradient);
}
// Listen to all sliders
angleSlider.addEventListener('input', updateGradient);
startPosSlider.addEventListener('input', updateGradient);
endPosSlider.addEventListener('input', updateGradient);
// Set initial state
updateGradient();
</script>
Note: CSSLinearGradient and other gradient typed objects may have varying browser support. For broader compatibility, you can construct the gradient as a string and set it via attributeStyleMap.set('background-image', gradientString), which still benefits from Typed OM's parsing on read. The example above demonstrates the ideal typed approach when available.
Creating a Scroll-Aware Animation Controller
This example shows how to read transform values from an element, modify individual components based on scroll position, and write them back—all without string manipulation.
<div id="animated-card" style="
width: 200px;
height: 200px;
background: teal;
transform: translate(0px, 0px) rotate(0deg) scale(1);
position: fixed;
top: 50%;
left: 50%;
margin-top: -100px;
margin-left: -100px;
"></div>
<script>
const card = document.getElementById('animated-card');
function updateTransformsBasedOnScroll() {
const scrollY = window.scrollY;
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
const scrollFraction = Math.min(scrollY / maxScroll, 1);
// Read current transform as typed value
const styleMap = card.computedStyleMap();
const currentTransform = styleMap.get('transform');
// Clone components, modifying them
const newComponents = [];
for (const component of currentTransform) {
if (component instanceof CSSTranslate) {
// Move element based on scroll progress
const xOffset = new CSSUnitValue(scrollFraction * 100, 'px');
const yOffset = new CSSUnitValue(scrollFraction * 50, 'px');
newComponents.push(new CSSTranslate(xOffset, yOffset));
} else if (component instanceof CSSRotate) {
// Rotate up to 360 degrees based on scroll
const rotation = new CSSUnitValue(scrollFraction * 360, 'deg');
newComponents.push(new CSSRotate({ angle: rotation }));
} else if (component instanceof CSSScale) {
// Scale from 1 to 1.5
const scaleAmount = new CSSUnitValue(1 + scrollFraction * 0.5, 'number');
newComponents.push(new CSSScale(scaleAmount, scaleAmount));
} else {
// Pass through unknown components unchanged
newComponents.push(component);
}
}
const newTransform = new CSSTransformValue(newComponents);
card.attributeStyleMap.set('transform', newTransform);
}
window.addEventListener('scroll', updateTransformsBasedOnScroll, { passive: true });
updateTransformsBasedOnScroll();
</script>
Building a Real-Time Calc Expression Editor
This example demonstrates traversing and modifying a calc() expression tree. Imagine a layout where you want to adjust the subtracted padding value dynamically.
<div id="responsive-panel" style="
width: calc(100% - 40px);
height: 100px;
background: coral;
"></div>
<label>Subtract amount (px):
<input type="range" id="subtract-slider" min="0" max="100" value="40">
</label>
<script>
const panel = document.getElementById('responsive-panel');
const slider = document.getElementById('subtract-slider');
// Set up initial calc value via Typed OM
function buildCalcExpression(subtractPx) {
const hundredPercent = new CSSUnitValue(100, 'percent');
const subtractValue = new CSSUnitValue(subtractPx, 'px');
const negated = new CSSMathNegate(subtractValue);
return new CSSMathSum(hundredPercent, negated);
}
// Apply initial value
panel.attributeStyleMap.set('width', buildCalcExpression(40));
slider.addEventListener('input', () => {
const pxValue = parseInt(slider.value, 10);
// Instead of string manipulation, read and modify the typed tree
const currentWidth = panel.computedStyleMap().get('width');
if (currentWidth instanceof CSSMathSum) {
// The second term is the negated pixel value
// We can rebuild with the new value
const newExpression = buildCalcExpression(pxValue);
panel.attributeStyleMap.set('width', newExpression);
}
});
</script>
Best Practices and Performance Considerations
Typed OM is designed with performance in mind, but to maximize its benefits, follow these guidelines:
- Batch your writes — While Typed OM reduces per-operation overhead compared to string manipulation, multiple style changes still trigger separate style recalculations. Group related changes together where possible, or consider using
requestAnimationFramefor visual updates. - Prefer
computedStyleMap()for reads — The computed style map gives you the final, resolved values after all CSS rules have been applied. TheattributeStyleMaponly reflects inline styles. Know which one you need. - Cache typed values when looping — If you're reading the same property repeatedly (e.g., in an animation loop), call
computedStyleMap().get()once and work with the returned object, rather than querying it on every frame. - Use typed construction over string setting — Setting
attributeStyleMap.set('width', '100px')still triggers internal parsing. Constructing anew CSSUnitValue(100, 'px')skips that parsing step entirely. - Check for
CSSUnparsedValuefallbacks — Not all CSS values are fully typed yet. Always check the type of a value you retrieve and handleCSSUnparsedValuegracefully when the browser falls back to string tokens. - Leverage
CSSMathValuefor dynamic layouts — Instead of manually recalculating pixel values in JavaScript when container sizes change, buildcalc()expressions that the browser evaluates natively. This keeps layout logic in the CSS engine where it belongs. - Register custom properties with
CSS.registerProperty— To get typed values for custom properties, register them with a syntax descriptor. Without registration, they remain asCSSUnparsedValue.
Browser Support and Feature Detection
Typed OM is part of the CSS Houdini specification and has been shipping in Chromium-based browsers (Chrome, Edge, Opera) since version 66+. As of 2024, Firefox and Safari have partial implementations behind feature flags. Always use feature detection:
function supportsTypedOM() {
return typeof CSSStyleValue !== 'undefined'
&& typeof CSSUnitValue !== 'undefined'
&& typeof StylePropertyMapReadOnly !== 'undefined'
&& 'computedStyleMap' in Element.prototype
&& 'attributeStyleMap' in Element.prototype;
}
if (supportsTypedOM()) {
// Use Typed OM
const map = element.computedStyleMap();
// ...
} else {
// Fall back to traditional string-based manipulation
const widthStr = getComputedStyle(element).width;
// Parse manually...
}
For broader compatibility, consider using the Houdini polyfill libraries or structuring your code so that Typed OM usage is an enhancement. The traditional string-based approach remains a reliable fallback.
Conclusion
CSS Typed OM represents a fundamental shift in how JavaScript interacts with stylesheets. By replacing opaque strings with structured, typed objects, it eliminates entire categories of bugs related to unit conversion, string parsing, and format ambiguity. The ability to traverse calc() expression trees, manipulate transform components individually, and perform mathematical operations on typed values opens up new patterns for dynamic styling that were previously impractical or impossibly fragile.
The key to adopting Typed OM effectively is to think in terms of the object hierarchy: CSSUnitValue for simple values, CSSMathValue subclasses for expressions, CSSTransformValue for transforms, and CSSKeywordValue for keywords. Use computedStyleMap() to read resolved values and attributeStyleMap to write inline styles. As browser support expands and more CSS value types become fully typed, Typed OM will become an indispensable tool in every web developer's toolkit—making CSS manipulation as robust and predictable as the rest of modern JavaScript development.