demo · v147 · CSS · computed values

Animation Pipeline Demo

Side-by-side CSS animations plus a live CSSOM probe. The left arena animates width alone; the right arena carries --edge-width into border-width and outline-width while style values flip through non-painting states. The probe below shows which values come from computedStyleMap() and which come from getComputedStyle().

ChromeStatus 5133099851317248 tracks decoupling for border-width, outline-width, and column-rule-width from their corresponding *-style values. This page keeps the animation controls, then checks the exact CSSOM surfaces that decide whether a hidden style erased the authored width or only the used paint width.

computed-value contract probe

4px
surface style read computedStyleMap() getComputedStyle()
border-width border-top-style: none get('border-top-width'): checking... borderTopWidth: checking...
outline-width outline-style: none get('outline-width'): checking... outlineWidth: checking...
column-rule-width column-rule-style: none get('column-rule-width'): checking... columnRuleWidth: checking...

The hidden-style branch checks whether the authored width is retained while a non-painting style keeps the border's used paint width at zero.

1200ms
12
decoupled — width only (Chrome 147 optimised)
style flips — width + border/outline style states
Decoupled (width only)
-- fps
Coupled (width + border-style)
-- fps
Decoupled avg fps
--
Coupled avg fps
--
Boxes per side
12
Animation duration
1200ms
Chrome 147 decoupled?
checking...

what's measured

Each arena tracks how many requestAnimationFrame callbacks fire per second as a local stress signal while the controls change duration and element count. The right arena uses a registered --edge-width chain for border-width and outline-width. The normative evidence is the contract probe above: it reads the exact border-width, outline-width, and column-rule-width surfaces instead of inferring support from a generic animation result.

On fast hardware the FPS difference may be small at low box counts. Increase the slider to 50 boxes to make the style-flip branch easier to compare.

the CSS

@property --edge-width {
  syntax: "<length>";
  inherits: false;
  initial-value: 4px;
}

/* Decoupled track: width animates independently. */
@keyframes widen-decoupled {
  0%   { width: 20px; }
  50%  { width: 200px; }
  100% { width: 20px; }
}

/* Style-flip track: custom-property width survives non-painting styles. */
@keyframes widen-coupled {
  0%   { width: 20px;  --edge-width: 4px; border-style: none; }
  50%  { width: 200px; --edge-width: 8px; border-style: solid; }
  100% { width: 20px;  --edge-width: 4px; border-style: none; }
}

.box-coupled {
  border-width: var(--edge-width);
  outline-width: var(--edge-width);
}

const computed = box.computedStyleMap().get('border-top-width');
const usedPaint = getComputedStyle(box).borderTopWidth;
// usedPaint may be 0px under border-style:none even when computed is 4px.

see also

implementation reference

Need the exact API surface, compatibility boundaries, errors, lifecycle, and source links? Read the matching gendn reference ↗