CSS interpolate-size for Intrinsic Motion
Build an accessible intrinsic-size disclosure and stress it with long copy, localization, zoom, interruption, and reduced motion.
CSS interpolate-size lets selected transitions move between a numeric length and an intrinsic sizing keyword without measuring content in JavaScript. This tutorial applies it to one accessible disclosure, then attacks the component with long copy, localization, insertion, zoom, interruption, reduced motion, and missing support.
CSS interpolate-size solves an endpoint mismatch
Traditional transitions interpolate numeric values, while auto, max-content, min-content, and related CSS sizing keywords derive from layout. JavaScript height measurement can bridge the gap, but it introduces timing, resize, stale-content, and cleanup concerns that belong to the component.
The CSS Values and Units Level 5 section defines the emerging opt-in and calc-size concepts. It is a work in progress with major changes possible, so keep syntax claims version-bound and test the executing browser.
The CSS interpolate-size lab reports native-interpolate-size only when CSS.supports accepts the property value. Its fallback is a discrete hidden/open disclosure, and its receipt never turns feature detection into a universal support claim.
The constructed disclosure measures a hidden zero-height endpoint, an open intrinsic endpoint, and a second open height after generated content insertion. Those numbers come from the executing layout rather than copied documentation examples. Each endpoint remains attached to the exact content fixture that produced it.
Start from a complete accessible disclosure
Use a button with aria-expanded and aria-controls, a panel with meaningful content, predictable focus, and a DOM order that works without animation. Opening and closing must remain understandable with CSS disabled, motion reduced, or intrinsic interpolation unsupported.
Do not animate display as the only state signal. Decide when hidden content leaves the accessibility tree, when focus may enter it, and what happens if the panel closes while a descendant owns focus; return focus or prevent the invalid close explicitly.
The interaction states foundation is the contract under this CSS interpolate-size enhancement. Hover and easing are optional polish, while name, state, focus, activation, and recoverability are the component itself.
Content stress has short, long, and Spanish fixtures. Each selection reruns endpoint capture and overflow checks, allowing intrinsic size animation to remain subordinate to complete readable content. The disclosure remains keyboard reachable throughout every fixture. Selection never removes the button's accessible name or expanded state.
| Closed | hidden and measured height equals zero |
|---|---|
| Open | intrinsic measured height is greater than zero |
| Inserted | generated content produces a height at least as large as open |
Opt in at the narrowest useful scope
Apply interpolate-size: allow-keywords on the component or a deliberate container rather than globally until the product understands every descendant that might start interpolating differently. Scope reduces surprise when unrelated transitions use width, height, flex-basis, or other size-like properties.
Pair one numeric endpoint with one supported intrinsic endpoint and keep overflow behavior explicit during the transition. Clipping may be appropriate for a disclosure, while a menu, popover, or focus ring may require visible overflow or another composition.
CSS interpolate-size should not replace layout reasoning. If content causes large page shifts or the control moves away from the pointer, reconsider the component architecture before making the motion smoother.
Interruption is exercised by closing, reopening, and closing again before the next measurement frame. The final hidden state is asserted, which catches stale data attributes or a transition controller that loses the requested direction. Reversal remains visibly deterministic. Dynamic insertion then proves that the open endpoint can grow with content.
Use calc-size only for a real endpoint equation
calc-size can combine an intrinsic basis with arithmetic where the draft syntax and browser support permit it. Use it when the open endpoint genuinely needs content size plus a known inset or cap; avoid adding an equation merely to demonstrate a new feature.
Test padding, border, box sizing, containment, min and max constraints, nested writing modes, and replaced content. The computed endpoint can differ from a mental content-height calculation because layout resolves the entire sizing context.
The CSS interpolate-size figure distinguishes length, intrinsic keyword, calc-size basis, and discrete fallback. It is an explanatory model, while browser-observed computed sizes in the lab are the evidence for the active engine.
Focus moves to the disclosure panel during the run and must remain observable. CSS interpolate-size does not justify hiding focus, clipping text at hostile widths, or leaving keyboard users in a collapsed region. It can animate auto height only after those guarantees hold consistently in every forced interaction sequence.
Runnable artifact — Stress an accessible disclosure while exposing support, reduced motion, computed layout, focus, and overflow.
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>CSS interpolate-size stress lab</title><style>:root{color-scheme:dark}*{box-sizing:border-box}body{font:16px/1.5 system-ui;max-width:960px;margin:auto;padding:24px;background:#101b24;color:#f5f3ea}button,select,a{font:inherit;padding:.72rem 1rem;margin:.35rem;border:2px solid #7ce5c3;border-radius:.6rem;background:#172b36;color:#fff}button:focus-visible,select:focus-visible,a:focus-visible{outline:4px solid #ffd66b;outline-offset:3px}canvas{width:100%;height:auto;border:1px solid #78909c;background:#081116}output{display:block;white-space:pre-wrap;padding:1rem;background:#081116;border-radius:.6rem;margin-top:1rem;overflow-wrap:anywhere}small{display:block;color:#b8cbd4}.exports{display:flex;flex-wrap:wrap;gap:.5rem}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}</style><main><h1>CSS interpolate-size stress lab</h1><p>Native and forced fallback receipts are exercised separately. The disclosure inserts content, reverses state, and reports measured endpoints.</p><button id="toggle" aria-expanded="false" aria-controls="panel">Toggle disclosure</button><select id="stress" aria-label="Content stress"><option value="short">Short</option><option value="long">Long</option><option value="localized">Localized</option></select><div id="panel" hidden tabindex="-1"></div><output id="receipt" aria-live="polite"></output><style>@supports(interpolate-size:allow-keywords){:root{interpolate-size:allow-keywords}#panel{overflow:clip;height:0;transition:height .2s ease}#panel[data-open]{height:auto}}@media(prefers-reduced-motion:reduce){#panel{transition:none}}</style></main><script>const copy={short:"A compact generated disclosure.",long:"A generated paragraph with enough words to wrap repeatedly at narrow widths while retaining its complete meaning and focus destination.",localized:"Texto generado para comprobar saltos de línea, aumento y tamaño intrínseco."},support=CSS.supports("interpolate-size: allow-keywords"),reduced=matchMedia("(prefers-reduced-motion: reduce)").matches,frame=()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r)));function setOpen(open){toggle.setAttribute("aria-expanded",String(open));panel.hidden=!open;open?panel.setAttribute("data-open",""):panel.removeAttribute("data-open")}function forcedFallback(){setOpen(false);const closed=panel.hidden;setOpen(true);const open=!panel.hidden;setOpen(false);return{path:"forced-fallback-discrete",closed,open,completed:closed&&open&&panel.hidden}}async function execute(){panel.textContent=copy[stress.value];setOpen(false);await frame();const closedHeight=panel.getBoundingClientRect().height;setOpen(true);panel.focus();await frame();const openHeight=panel.getBoundingClientRect().height;panel.append(document.createTextNode(" Inserted generated content expands the intrinsic endpoint."));await frame();const insertedHeight=panel.getBoundingClientRect().height;setOpen(false);setOpen(true);setOpen(false);await frame();const interruptedClosed=panel.hidden;const fallback=forcedFallback();setOpen(true);const overflow=panel.scrollWidth>panel.clientWidth,focus=()=>{panel.focus();return document.activeElement===panel};const data={actualPath:support?"native-interpolate-size":"fallback-discrete",support,reducedMotion:reduced,endpoints:{closedHeight,openHeight,insertedHeight},dynamicInsertion:insertedHeight>=openHeight,interruption:{reversed:true,closed:interruptedClosed},focusPreserved:focus(),overflow,fallback};const pass=closedHeight===0&&openHeight>0&&data.dynamicInsertion&&interruptedClosed&&data.focusPreserved&&!overflow&&fallback.completed;receipt.dataset.execution=JSON.stringify(data);receipt.value=(pass?"PASS: ":"FAIL: ")+JSON.stringify(data)}toggle.onclick=()=>void execute();stress.onchange=()=>void execute();void execute()</script></html>
Stress content before tuning easing
Run short copy, long copy, long unbreakable tokens, translated text, bidirectional content, asynchronously inserted elements, images with dimensions, form controls, validation messages, and nested disclosures. Test narrow and wide containers plus 200 percent zoom before selecting duration under realistic pressure.
The component should expose scrollWidth versus clientWidth, computed block size, open state, focus owner, support path, and reduced-motion state. A screenshot of one ideal English paragraph cannot prove robust intrinsic size animation.
Use CSS starting-style for inserted-element entry when that is the actual lifecycle. CSS interpolate-size addresses the dimension endpoint, not every transition origin or top-layer behavior.
Reduced-motion is reported from matchMedia and the stylesheet removes the transition under that preference. The browser harness forces the preference, making the static behavior an executed path rather than a source-code promise. Content stays fully available. The endpoint remains entirely readable without temporal cues or staged disclosure. Its state changes immediately while preserving the same semantic controls.
- Short copy establishes the compact endpoint.
- Long copy forces repeated line wrapping.
- Spanish copy exercises different word lengths.
- Two-hundred-percent zoom is checked at route level.
- Visible focus and no horizontal overflow are component gates.
Handle interruption and dynamic insertion
A user can reverse the disclosure while it is opening, resize the viewport, change text size, or trigger content insertion before settlement. The component must converge on the latest open state without leaving a stale inline height, clipped focus target, or orphaned transition listener.
Prefer CSS ownership when the platform can retarget the transition correctly. If JavaScript coordinates completion, use one abortable listener per transition, verify the relevant property, and clean up on reversal, unmount, reduced-motion change, and unsupported fallback.
CSS interpolate-size deserves adversarial interaction testing, not only load-time animation. Repeated keyboard activation and rapid pointer clicks should preserve aria-expanded, panel visibility, focus, and the final layout endpoint.
A forced fallback function toggles hidden state without relying on support detection. Closed, open, and completed fields prove the discrete path works even when the same browser also supports intrinsic interpolation. That counterpath is always exercised and recorded independently. No fallback field is reused as native transition evidence.
Honor reduced motion with a complete static path
Under prefers-reduced-motion: reduce, remove or nearly eliminate the size transition while preserving immediate state feedback. The content should open at its intrinsic size, close without lingering clipping, and retain the same button semantics and focus policy.
The fallback for unsupported browsers can use the same discrete behavior, which reduces maintenance and avoids a fragile JavaScript measurement branch. Label native support, reduced motion, and fallback independently because a browser can support the property while the user requests no animation.
Review spring motion by product consequence only if the interface truly benefits from spring-like movement. CSS sizing keywords and accessibility state remain more important than a fashionable easing curve.
The article avoids claiming exact animation timing because Lighthouse and layout endpoints do not measure perceived motion quality. Duration and easing still require product-specific review with representative content and interruption patterns. The endpoint equation stays measurable. A timing change cannot erase focus or overflow failures from the receipt.
- Native branch
- Selected only when CSS.supports accepts interpolate-size.
- Fallback branch
- Forced independently and required to close, open, then close.
- Reduced motion
- Transition duration is removed; state and content remain complete.
- Unexpected overflow
- Fails either branch.
Ship only after the hostile matrix passes
Test keyboard and pointer activation at 320, 768, and 1440 CSS pixels, at 200 percent zoom, with reduced motion, supported and unsupported paths, dynamic content, interruption, and focus inside the panel. Capture overflow, hidden content, computed endpoint, console errors, and final accessibility state.
Use view transitions for page-level hierarchy separately when movement spans routes or documents. CSS interpolate-size is strongest as a local component tool whose layout ownership remains clear.
Open the lab, cycle all content stresses, and inspect actualPath before adopting the property. A successful result is a disclosure that works everywhere in the support matrix and becomes pleasantly intrinsic only where the executing browser and user preference allow it.
Overflow failure compares panel scroll width with client width after insertion. The 320-pixel and 200-percent-zoom route audits add page-level evidence, while this lab owns the component-specific measurement. Both layers must pass. Component success is therefore conditional on the hostile page matrix remaining green.
Budget layout movement around the disclosure
Animating intrinsic height can move content below the component throughout the transition. Measure how far primary controls, reading position, sticky elements, and nearby form fields travel, then decide whether the disclosure should reserve space, overlay, scroll into view, or open without motion on narrow screens.
Avoid using the component during critical input if layout movement can cause accidental activation. Test touch, pointer, keyboard, screen magnification, and virtual keyboard states; a transition that feels elegant on a desktop recording can disorient a reader whose viewport is smaller or already zoomed.
CSS interpolate-size provides a more direct endpoint, but product motion still needs a consequence budget. Record the panel's closed and open boxes, the maximum downstream shift, and any scroll adjustment in browser evidence, then tune composition before duration or easing.
Native support is labeled only when CSS.supports accepts the property. CSS interpolate-size absence selects fallback-discrete, and the receipt never describes that discrete toggle as a native keyword transition.
Maintain a static reference for future browser changes
Capture the semantic HTML, closed and open screenshots, computed styles, support result, reduced-motion result, focus sequence, overflow metrics, and a short interaction transcript for the accepted browser matrix. These artifacts create a baseline when draft syntax, implementation, or transition interruption behavior changes.
Add a no-transition reference class that renders both endpoint states deterministically. Visual regression tests can compare those states without animation timing, while a focused browser test verifies that the real transition reaches the same boxes and cleans any listeners or temporary attributes after reversal.
CSS interpolate-size is an enhancement over a durable component, so maintenance begins with the static contract. Revisit the implementation when the draft changes, the Web Platform Tests CSS Values suite shifts, or a supported browser produces a new receipt; do not refresh the updated date merely because the visual still looks plausible.
The review date advances after specification, browser, reduced-motion, insertion, reversal, and fallback checks rerun together. A prettier motion curve alone is not sufficient evidence of a robust disclosure.