Static HTML marketing sites still ship accordions, disclosure panels, and filter drawers that need to grow from zero to their natural height without hard-coding pixel tallies for every breakpoint. For years the honest answer was “measure with JavaScript and set explicit pixels,” because CSS treated height: auto as a non-interpolable keyword at computed-value time. In 2026, the combination of interpolate-size: allow-keywords and calc-size() finally gives designers a declarative bridge between intrinsic sizing and transition math, but only when you understand the cascade, the interaction with flex and grid min-size defaults, and the way WebKit schedules reflow. This article walks through the pain, the primitives, progressive enhancement, performance discipline, accessibility, and a practical matrix so your static bundles stay small while motion stays smooth. When you are ready to compare cross-document transitions versus in-page height tweens, read the static MPA View Transitions guide; when your disclosure also contains auto-growing form controls, pair this work with field-sizing for content-driven form controls so height animations do not fight textarea intrinsic sizing.
The economics of verification matter: reproducing subpixel layout on the same GPUs your customers use is cheaper on a rented Mac mini than shipping a broken animation to production. MacHTML advertises cloud Apple Silicon hosts near $16.9 per day, which is the price anchor we cite when discussing rehearsal budgets below.
Why animating height:auto has been painful
Classic CSS transitions interpolate lengths, percentages, and some transforms, but height: auto is special: its used value depends on content measurement that may not exist until after the first layout pass. If you declare transition: height 240ms ease while toggling between 0 and auto, many engines simply snap at the end of the interval because there is no numeric pair to interpolate. Developers responded with max-height: 9999px hacks, which feel smooth until translation inflates copy and your transition duration no longer matches perceptual distance, or until GPU memory balloons because the layout tree believes the box could be enormous. JavaScript measuring scrollHeight works, but it reintroduces main-thread coupling, complicates content security policies on static hosts, and breaks when web fonts finish loading mid-animation.
The new model keeps measurement inside the style system: you still think in intrinsic terms, but you expose a numeric corridor the engine can treat as interpolable. That shift matters for static HTML exports where bundlers forbid React state machines and where every byte in main.js is suspect. It also matters for Safari-first teams, because WebKit historically guarded against infinite oscillation when percentage heights and intrinsic keywords mixed inside flex items. Understanding those guardrails prevents you from filing “browser bug” reports that are actually spec-correct cyclic resolution behavior.
Another subtle pain is coexistence with overflow: hidden for rounded corners: clipping rectangles animate with the border box, but if you rely on max-height, the shadow and outline paint phases may desynchronize from the content reveal, producing a “double door” flicker on retina displays. Intrinsic interpolation reduces that mismatch because the used height tracks the measured content box more tightly, assuming you avoid animating unrelated properties concurrently.
interpolate-size: allow-keywords in practice
The interpolate-size property opts a subtree into keyword interpolation between used values that were previously non-numeric. Setting interpolate-size: allow-keywords on a disclosure root tells the engine it may treat transitions between intrinsic keywords and lengths as measurable endpoints, provided the other longhands cooperate. In authoring terms, you usually place it on .disclosure or details wrappers rather than the global body, because allowing keywords everywhere widens the interpolation surface and can make unrelated rules accidentally expensive.
.disclosure {
interpolate-size: allow-keywords;
overflow: clip;
transition: height 260ms cubic-bezier(.2,.8,.2,1);
}
Pair the property with explicit height states: collapsed might be 0, expanded might still be auto, but now the engine can compute a numeric trajectory instead of giving up. If you toggle classes with height: 0 versus height: auto, watch for margin-collapsing interactions: collapsed blocks with adjacent margins may still produce a non-zero fragment, so use padding on an inner wrapper instead of margin on the animating element when QA reports “mystery gaps.”
Documentation teams should note that interpolate-size inherits in the cascade, which is convenient for nested accordions but risky inside component libraries. If a child card accidentally inherits allow-keywords while also animating flex-basis, you can trigger multi-pass layout in Safari when both axes resolve against indefinite containers. Scope the property with a utility class and remove it from static shells that do not animate.
calc-size(fit-content) as the measurement bridge
calc-size() wraps an intrinsic size keyword with a calculation that produces a length the transition system can sample. A common accordion recipe sets the expanded state to calc-size(fit-content, size) (using the spec’s keyword form) so the browser measures the content box as if it were height: auto, then feeds that measurement into interpolation. Collapsed remains 0 or a fixed header height, and the transition sees two lengths instead of a length and a keyword.
.panel[data-open="true"] .panel-body {
height: calc-size(fit-content, size);
}
.panel[data-open="false"] .panel-body {
height: 0;
}
Authors migrating from max-height hacks should compare perceived velocity: intrinsic calc-size() tracks real content depth, so easing curves feel honest on short answers and long policy text alike. The downside is sensitivity to dynamic content: if a live region appends text while the transition runs, the target endpoint moves, which is correct behavior but can stretch durations if your easing assumes a fixed distance. For static HTML, freeze DOM updates until transitionend fires unless you explicitly support mid-flight reflow.
When combining calc-size() with padding, remember that height on a box with multiple padding longhands still uses the content box model you specify via box-sizing. Static templates often set * { box-sizing: border-box; }; verify that your measured height includes the border box you intend, otherwise Safari will animate the content height while borders appear to pop in a single frame.
Progressive enhancement with @supports
Ship a layered fallback: if calc-size() is unavailable, keep the disclosure functional with an instant toggle or a max-height clip. Feature queries should test the function token, not just the property, because partial implementations may parse but not interpolate.
@supports (height: calc-size(fit-content, size)) {
.panel-body { transition: height 240ms ease; }
}
@supports not (height: calc-size(fit-content, size)) {
.panel-body { transition: max-height 320ms ease; max-height: 0; }
.panel[data-open="true"] .panel-body { max-height: 80vh; }
}
Static sites benefit from printing the support result into an HTML comment during build for support teams, but never gate content exclusively on modern CSS: accordions should still expose headings and anchors for no-CSS clients. Treat motion as optional enhancement layered on top of semantic details/summary or button-controlled panels with aria-expanded.
Grid, flex, and min-height:auto interactions
Flex items default to min-height: auto, which prevents them from shrinking below their content’s minimum contribution. If you animate a flex child from 0 to an intrinsic height, you may need min-height: 0 on that item to allow the collapsed state to be honest without overflow spam. Grid has analogous behavior with min-size:auto and track sizing: an animated row may refuse to collapse if in-flow grid items report large intrinsic minimums.
When a disclosure sits inside a CSS grid area with align-self: stretch, the grid item’s block size is definite even if the content wants to be intrinsic. That definiteness changes how calc-size() resolves, sometimes producing faster layout than flex contexts but occasionally causing aspect-ratio clamps to fight the height animation. For static marketing pages, prefer a single-column grid for the article body and isolate animated panels in their own formatting context with contain: layout once you confirm focus rings are not clipped.
Another grid footgun is mixing gap animations with height: gaps recompute when tracks move, so Safari may run two dependent layouts per frame if you also animate margin-block-end on siblings. Keep sibling margins static while height tweens, then fade opacity if you need a softer entrance.
Safari and WebKit implementation notes
WebKit’s animation engine coalesces style updates when the main thread is busy, which means your height transition may jump if heavy JavaScript blocks painting even on fast M3 machines. Static HTML should not block, but embedded analytics snippets can still jank: defer third-party tags below the fold or behind idle callbacks. Safari Technology Preview often exposes newer interpolation behavior before stable Safari; if your release calendar spans a macOS upgrade wave, test both channels and snapshot pixel diffs.
Hardware acceleration does not magically move height to the compositor: height still drives layout. Combining transform: translateY() with height animations doubles layout+paint work unless you hoist the moving layer with will-change: transform sparingly. Prefer one motion primitive per gesture: either slide with transform or reveal with height, not both stacked on the same element without profiling.
For subpixel text during the reveal, ensure antialiased fonts do not rerasterize wildly: if -webkit-font-smoothing differs between states, QA may report “shimmering” copy that is actually bitmap cache invalidation. Keep font settings identical across collapsed and expanded branches.
Performance: layout thrash and coalescing
Each height sample forces layout because geometry changes. Opening five accordions simultaneously multiplies work. Cap concurrent animations, stagger with modest delays, or mark non-visible panels content-visibility: auto once they leave the viewport to trim work. Measure with WebKit’s timeline: watch for purple layout bars wider than one frame.
Also audit ResizeObserver callbacks if you still use JS fallbacks: firing layout reads inside observers while CSS transitions run can create feedback loops. If observers must exist, throttle them with requestAnimationFrame and skip writes while document.hidden is true.
Accessibility and prefers-reduced-motion
Some users experience vestibular discomfort when large regions move. Honor prefers-reduced-motion: reduce by collapsing transition durations to nearly zero or swapping to an opacity-only hint. Do not remove content; preserve the same DOM order and focus behavior.
@media (prefers-reduced-motion: reduce) {
.panel-body { transition: none !important; }
}
Keyboard users rely on predictable focus rings: if your animated panel clips focus outlines, use overflow: clip with inner focusable padding or move outlines to an inner wrapper. Screen readers should hear expanded state via aria-expanded toggled synchronously with the attribute change, not only after the animation completes, unless your UX explicitly delays announcements (generally discouraged).
Static HTML patterns for accordions and drawers
For static exports, prefer a single class on a container and drive state with data-open attributes set by minimal JS or native details behavior mirrored with CSS. If you must be JS-free, details offers open/close without scripts, but styling animations historically required hacks; interpolate-size plus calc-size() finally align native semantics with motion. When mixing, ensure only one system owns height—duplicate rules fight.
Author CSS in layers: base typography, then layout, then motion. That ordering prevents accidental specificity wars where a late !important from a marketing override disables transitions on launch day. Static hosts like S3/CloudFront lack server-driven feature flags, so keep motion toggles in a single motion.css file versioned with a query string for cache busting.
Decision matrix: when to animate height intrinsically
| Scenario | Use intrinsic height tween? | Notes |
|---|---|---|
| Legal accordion with long copy | Yes | calc-size tracks actual depth; avoid max-height guesses. |
| Infinite feed cards | Rarely | Virtualize instead; height animation thrashes scroll metrics. |
| Modal dialogs | Sometimes | Prefer transform for enter/exit; reserve height for content reflow. |
| Sticky nav reveal | Caution | Interact with position: sticky compositing; test Safari. |
Numbered release checklist for motion QA
- Verify collapsed and expanded heights at 320px, 390px, and 834px breakpoints with longest locale strings.
- Toggle
prefers-reduced-motionin macOS Accessibility and confirm panels still communicate state. - Open five panels rapidly; ensure CPU does not spike above comfortable laptop fan curves on baseline M2.
- Record WebKit timeline traces with third-party scripts enabled as in production.
- Validate focus order and visibility inside clipped containers during the transition midpoint.
- Compare stable Safari and STP builds if your launch spans a macOS release window.
- Re-test after webfont preload changes; font swaps alter intrinsic measurements.
- Capture pixel diffs for dark mode and increased contrast settings.
FAQ
Is max-height still acceptable?
Yes as a fallback, but prefer intrinsic calc-size when supported to avoid timing mismatches on long content.
Does this replace scroll-driven animations?
No; scroll timelines solve different problems. Combine thoughtfully and test interaction.
Can I animate width:auto similarly?
Many of the same concepts apply on the inline axis, but horizontal motion triggers more reflow for text; profile carefully.
Shipping trustworthy motion on static HTML is less about memorizing syntax and more about rehearsing on real Safari hardware with production-like fonts, extensions, and display scaling. A Mac mini rented from MacHTML for about $16.9 per day gives your team an always-on target that mirrors customer machines, complete with SSH for automated pixel snapshots and VNC for designer review sessions.
Rehearse intrinsic height animations on cloud Mac mini
Open your static HTML bundle on real Apple Silicon Safari, profile layout during accordion storms, and sign off before you merge motion tokens into production CSS.