Motion blur gives fast-moving elements a real shutter smear. For each frame, HyperFrames reads where every mover was across the shutter interval and blurs it along that path. A box sliding across the frame leaves a streak instead of a sharp copy at one position. It is opt-in. Nothing blurs until you add data-motion-blur="on" to the composition root.

Turn it on

That is all. The same attribute drives the browser editor preview and hyperframes render.

In the browser editor

The Blur tab in the browser editor’s right dock sets it with one control: Off, Subtle or Strong. Subtle is the default blur (240° shutter, samples sized to the smear). Strong opens the shutter to 720° and the samples to 64. The panel writes the attributes above to the composition root, so a render uses the same setting, and Undo works. Subtle and Strong also turn on data-motion-blur-events, so motion set from a render(t) callback is found. Off removes data-motion-blur and data-motion-blur-events and leaves the other attributes where they are.

What it blurs

Elements that move quickly in 2D: position, scale and rotation changes from GSAP, CSS animations and transitions, and the Web Animations API. Slow movers and static content are left sharp. A mover’s glow and shadow travel with it, and the background behind it stays where it is.

Two ways it is drawn

Filter puts an SVG filter on each mover: copies of the element along its path, averaged, and softened along the motion. It is what data-motion-blur-tier="auto" (the default) draws with. It works in any Chrome and over a WebGL canvas. On a four-whip test it cost about what a render without blur costs; on two real films it cost more than capture (see Cost and limits). Turns and zooms are only softened, not streaked. Capture is opt-in with data-motion-blur-tier="capture". It copies each mover out of the page with Chrome’s HTML in Canvas API and redraws it along its path. It draws turns and zooms truthfully: a spinning tile smears along its arc and its centre stays sharp, and a zoom streaks outward. It has limits of its own, listed here (the filter tier’s are listed under Cost and limits, and a few, such as backdrop-filter, apply to both), so ask for it only for a film whose spins and zooms need the streak:
  • Anything in front of a mover is lost: a static panel, an overlay or a title card over a moving element is drawn behind its smear.
  • mix-blend-mode and backdrop-filter on a mover or its ancestor are not applied.
  • A CSS selector that reaches the root through its parent, such as body > #root > .m, stops matching once capture wraps the root.
  • A mover sliding into an overflow: hidden or clipped card drags the clip edge along, two overlapping movers carry each other’s pixels, and content past the frame edge leaves a thin band.
  • In preview, the first frame after every play shows black for a moment.
Capture needs a browser with HTML in Canvas. Chrome 157 and 158 builds exposed it without a flag in headless and Chrome-for-Testing builds; whether stable Chrome does is unverified. Chrome 155 and earlier need chrome://flags/#html-in-canvas. A page that asks for capture gets the filter tier instead when capture cannot run: This is decided once for the whole composition, from the first frame. A defect the scan misses (a WebGL canvas added late, say) is caught per frame instead: that one frame is rendered sharp and counted in failedFrames. Blur is off, and stays off, for HDR renders and renders that use the engine’s own sub-frame blur. Distributed renders (plan, renderChunk, assemble, as the AWS Lambda and Google Cloud Run adapters run them) and --start/--end range renders blur like a whole local render. Pass --page-motion-blur off (or pageMotionBlur: "off" in a distributed config) to render them sharp.

Find out which one is drawing

In the page, window.__hfMotionBlur.report() returns state, tier, reason, reasonCode and movers. A render logs one line per session with the tier, and a session-end line when any frames fell back to sharp (failedFrames > 0).

Cost and limits

  • Render time. It varies by film. On an 8 s test composition with four whips, a render took 9.8 s with capture, 5.2–5.4 s with the filter tier and 5.3–5.9 s with no blur (two runs each, including startup). On two real 1080p films the filter tier cost more than capture, measured as CPU seconds of the whole render (wall time was too noisy on a loaded machine): spacex-launch (77 movers, 39.5 s) took 547–552 CPU s with the filter tier, 302–320 with capture and 184–193 with no blur (three runs each, 2.9 times no blur); k3-promo (106 movers, 16.5 s) took 236, 205 and 117 (one run per arm, 2.0 times no blur). The two films do not order by mover count, and the cause of the difference was not isolated. The filter tier draws each mover as at least 32 offset copies per frame; capture adds a second paint per blurred frame.
  • Distributed renders. Blur applies in every chunk, drawn with the filter tier on auto. Measured on two real 1080p films with plan, renderChunk and assemble at 240 frames per chunk, in a software-GPU Chrome 153 on a machine other work was loading (load average 12 to 25), one run per arm: summed chunk CPU seconds went from 155 to 246 (1.6 times) on k3-promo and from 338 to 545 (1.6 times) on spacex-launch; summed chunk wall time rose 1.2 and 1.4 times. A film with data-motion-blur="on" also plans screenshot capture instead of BeginFrame capture, and the measured cost includes that switch.
  • Editing. The filter tier never wraps anything. With capture the root is wrapped only while playing or rendering; paused, it is plain DOM, so click-to-select works.
  • Capture only. Blurred preview can run at half the frame rate on heavy compositions, and a <video> drawn through the capture looks about 2–3% darker in the browser editor preview than in normal playback. Renders are not affected.
  • Preview and render. auto draws the same way in both at full preview quality. When a host lowers preview quality (Studio’s set-preview-quality message), the filter tier’s preview draws 16 copies at 1/2 and 8 at 1/4 and below; renders always draw 32. Against a sub-frame reference, 16 copies stayed within measurement noise of 32. 8 copies differ visibly on turning content. A page that asks for capture previews with the filter tier in a browser without HTML in Canvas, so a spin or zoom looks softer there than in the render.
  • Worker count. Output is not bit-identical between worker counts (around 60 dB PSNR), about the size of the difference chunking already causes.
  • Not handled. Pose errors under CSS zoom, position: fixed and foreignObject; a very short Web Animations API animation that falls between scan samples; a large see-through mover (a full-frame translucent sweep), which is left sharp while its fast children still blur. With capture, a mover is drawn above the static content, so a static element that should cover a mover does not.
  • Limits of the filter tier (the default), as measured on one-pattern test compositions:
    • ::before or ::after content outside the mover’s box is cut off.
    • A mover’s own clip-path or mask gets hard edges: the smear is clipped to the shape it has at that frame.
    • A position: fixed child of a mover that moves via left jumps, because the filter makes the mover its containing block.
    • A :last-child selector on the root’s children also matches the <svg> the tier adds to the root (nth-last-* works the same way but was not measured).
    • A visibility toggle inside the shutter window is drawn at full strength instead of half.
    • backdrop-filter on a mover differs from the 32-sample oracle in the sweep (the backdrop only).