Confession: I did not attraction overmuch astir shot arsenic precocious arsenic May. But by the clip España lifted the trophy past month, I was watching matches I had nary liking in, still incapable to explicate offside.
Somewhere successful 1 of those matches, I noticed what the image connected the surface sometimes did: the grassy transportation slides into 1 area and scales down, and the abstraction astir it opens up and fills pinch a sponsor card, a 2nd angle, aliases the workplace desk. You don’t miss a infinitesimal of the game.

I'd seen that effect a bajillion times without really looking excessively difficult astatine it. It's conscionable what tv does. But during 1 of my infamous 2am can’t-sleep iPhone investigation sessions, I learned it really has a name: a squeezeback.
The closest point I’ve seen for this effect connected the web is simply a YouTube video wherever personification squeezes the framework successful After Effects and renders it out. The layout is simply a picture, baked successful earlier upload, and the astir interactive it gets is simply a hotspot connected apical of a mobility schematic personification already designed.
Which is strange… because a compression is conscionable a layout change, and browsers are very bully astatine layout changes! Do it successful CSS and the abstraction that opens is simply a compartment you tin put a existent constituent in, chosen astatine playback alternatively of successful post.
LinkThe elasticity of a smooshy CSS grid
What happens if we put the subordinate successful the halfway compartment of a 3×3 grid wherever the six outer tracks are collapsed to nothing, past animate the way sizes?
A grid pinch different layouts
.stage { display: grid; grid-template-columns: 0fr 100fr 0fr; grid-template-rows: 0fr 100fr 0fr; overflow: hidden; transition: grid-template-columns 850ms cubic-bezier(0.65, 0, 0.35, 1), grid-template-rows 850ms cubic-bezier(0.65, 0, 0.35, 1); } .stage[data-layout='right-rail'] { grid-template-columns: 0fr 62fr 38fr; } .stage[data-layout='lower-third'] { grid-template-rows: 0fr 74fr 26fr; } .stage[data-layout='squeeze'] { grid-template-columns: 5fr 63fr 32fr; grid-template-rows: 0fr 82fr 18fr; row-gap: 8.9%; }Woah. That's an full mobility system! It doesn’t moreover request a toggle shape connected the video aliases scaling wrapper aliases requestAnimationFrame loop measuring anything. The video is simply a normal grid point successful a compartment that's getting smaller, and the panels parked successful the different cells get revealed arsenic their way values time off zero.
The uncover and the shrink are the aforesaid event. The sheet was ever sitting successful that compartment astatine afloat size. The shrink is conscionable the infinitesimal the compartment stops hiding it. Isn't CSS neat?
That row-gap: 8.9% is the only overseas number successful there. Percentage gaps resoluteness against height, and astatine 16/9, 8.9% of tallness is 5% of width, which is precisely the 5fr near column. The apical way is 0fr, truthful that 1 worth makes the abstraction supra and beneath astatine the aforesaid time. Equal inset connected 3 sides.
Unfortunately, you can't constitute it successful cqw and skip the mathematics since the shape is the query container, and a instrumentality can't query itself.
LinkThe timeline is simply a matter file
Video.js v10 is simply a React video subordinate constituent library: createPlayer, hooks, composable primitives, etc. but what it doesn't do is invent a scheduling system, because the browser already has a perfectly bully one: WebVTT.
cta-cues.vtt
WEBVTT away-kit 00:00:04.000 --> 00:00:12.000 right-rail champions-bundle 00:00:26.000 --> 00:00:34.000 squeezeEvery cue has an optional identifier statement correct supra the timestamps that isn’t really utilized very often, but it's cleanable for this usage case. The identifier becomes the merchandise key, the payload becomes the layout, and the transcript and pricing enactment successful your app keyed by that id. If you’re moving connected shoppable video, a merchandiser could retime the full acquisition by editing a matter file, and your pipeline tin make 1 per plus without a deployment.
You tin adhd it to the subordinate arsenic a <track> constituent and fto the browser show you erstwhile thing is active:
Video subordinate pinch cues attached
<MuxVideo src={src} autoPlay muted playsInline loop crossOrigin="anonymous"> <track kind="metadata" label="cta" src="/cta-cues.vtt" default /> </MuxVideo>Video.js v10’s usePlayer takes a selector arg, truthful selectTextTrack subscribes you to conscionable the matter way portion of the shop and thing else. That matters because you aren't re-rendering connected each timeupdate, and you get told erstwhile the way has really registered, because tracks travel and spell while the motor attaches.
Use the cues
import { usePlayer, selectTextTrack } from '@videojs/react'; const { textTrackList } = usePlayer(selectTextTrack); const fresh = textTrackList.some((t) => t.label === 'cta');The shop models tracks arsenic plain descriptors, truthful erstwhile it says yours exists, you tin cheque the unrecorded TextTrack to entree the cues themselves:
Handle the cuechange event
track.mode = 'hidden'; track.addEventListener('cuechange', () => { const cue = track.activeCues?.[0]; setActiveCue(cue ? { id: cue.id, layout: cue.text.trim() } : null); });A matter way has 3 modes: showing paints cues connected surface arsenic captions, disabled stops cuechange firing astatine all, and hidden parses the cues and fires the events without rendering thing - truthful that’s the 1 we’re utilizing to occurrence layout changes.
Once you're listening for cuechange alternatively of polling currentTime connected a timer, scrubbing backwards done a cue window, looping and seeking each behave correctly without you penning a statement for immoderate of them.
You tin past hindrance it to the DOM pinch 1 information attribute:
<div className="stage" data-layout={cue?.layout ?? 'full'}>
One warning: for now, you should build the cues successful a record alternatively than successful JavaScript, astatine slightest arsenic of penning this post. hls.js clears the cues disconnected each matter way erstwhile it attaches, and v10 ships a mixin that repairs the harm by uncovering the <track> constituent and reloading it. A way you created pinch addTextTrack() has nary constituent to reload, truthful it silently stays quiet while everything other looks correct. We should make that louder aliases hole it, but file-based VTT will activity for now.
LinkLet the video ray up the room
I’ve ever liked the gradient effect that my hue lights spill retired down my TV screen, matching the colors disconnected of the display. Let's create that here, too:
Glowing gradient canvas
<canvas ref={canvasRef} width={32} height={18} className="stage__ambient" /> const ctx = canvas.getContext('2d', { willReadFrequently: true }); let lastDraw = 0; const tick = (now) => { framework = requestAnimationFrame(tick); if (now - lastDraw < 100) return; lastDraw = now; if (!video.videoWidth) return; ctx.drawImage(video, 0, 0, 32, 18); }; frame = requestAnimationFrame(tick);We tin usage CSS to agelong a canvas crossed the shape and blur it into mush, truthful the revealed abstraction gets lit by immoderate is connected screen. It’s a beautiful performant solution too, truthful you don’t person to interest excessively overmuch astir the costs of implementing this effect.

Twinsies colors
willReadFrequently: true warns the browser you scheme to publication this canvas back, and without it the canvas lives connected the GPU wherever getImageData stalls each call. Also, videoRef has to constituent astatine the existent <video> element.
The Media entity you get backmost from useMedia() is simply a runtime-agnostic wrapper, and drawImage wants a CanvasImageSource, truthful handing it the wrapper leaves you pinch a blank canvas and thing successful the console to explicate why.
We tin moreover publication and usage the framework colors and propulsion an accent colour for the bargain fastener inheritance color.
My first effort astatine this averaged the red, green, and bluish channels and produced the aforesaid oliva brownish connected each frame, because the agleam entity and the acheronian shadows from the video cancel retired and onshore correct successful the mediate of the colour wheel.
The hole was to dainty the frame’s hue arsenic an perspective alternatively of conscionable an averaged number:
Sample colors from an angle
const weight = saturation * (1 - Math.abs(2 * lightness - 1)); x += Math.cos(hueRadians) * weight; y += Math.sin(hueRadians) * weight;There are 2 constraints for this to work: the subordinate needs crossOrigin="anonymous" and the root needs CORS headers, aliases getImageData throws an error. And it only useful connected unencrypted content, because Widevine and FairPlay frames can't beryllium drawn to a canvas astatine all, truthful you’d person to drawback the propulsion from drawImage and autumn backmost to a fixed gradient.
LinkTwo CSS gotchas
1. Grid fr values only interpolate correctly if the totals match. A track's rendered size is its worth divided by the full of each worth successful the template, truthful animating betwixt templates pinch different totals moves the numerator and denominator astatine abstracted times. My resting authorities was 0fr 1fr 0fr and my compression was 0fr 76fr 18fr, and the video did this connected the measurement back:
Something's fishy pinch this animation
0ms w=63% h=68% 365ms w=64% h=79% ← 365ms in, the width has moved 1 percent 630ms w=92% h=96% ← now it's sprinting 724ms w=100% h=100%So a azygous modulation moving crossed 2 axes pinch 2 wholly different curves created a squeezeback that looked for illustration it was being reeled successful connected a sportfishing line. Woah, Nelly! Instead, you person to lucifer the totals truthful the fraction moves linearly connected the curve you asked for.
2. @property fails silently: The Video.js default tegument resolves each area radius from 1 variable, truthful the video tin beryllium quadrate astatine afloat bleed and rounded erstwhile it floats free. That intends animating a length, and civilization properties don't interpolate astatine each until they're registered:
@property definition
@property --media-border-radius { syntax: '<length>'; inherits: true; initial-value: 2rem; }The initial-value of a registered property has to beryllium computationally independent, which rules retired utilizing rem, em, and percentages, and getting it incorrect invalidates the full rule. The tegument default is 2rem, truthful 2rem is what I wrote and collapsed the full thing. Write 32px alternatively and it works.
javascript
getComputedStyle(document.documentElement).getPropertyValue('--media-border-radius')LinkHow astir forms?
You tin usage this method to show much than elemental Buy Now buttons arsenic demo'ed successful the video astatine the apical of this post. In a squeezeback you tin usage the aforesaid sheet pinch different contents of your choice.
The layouts tin beryllium a lookup table
newsletter: { right: { kind: 'form', title: 'Match report, each Monday', body: 'One email a week. Goals, kits, and immoderate we conscionable shipped.', action: 'Subscribe', }, },The only rumor pinch this is if the cue expires mid-keystroke, the grid collapses the section retired from nether them, and the personification filling it retired wonders wtf conscionable happened to their signup form.
So the cue proposes the layout, and erstwhile the shape section is focused by the user, it overrides the exit.
Handling shape attraction wrong the squeezeback
export function useCueHeldByFocus(cue, containerRef) { const [focusHeld, setFocusHeld] = useState(null); const lastCue = useRef(null); if (cue) lastCue.current = cue; useEffect(() => { const onFocusIn = (event) => { if (containerRef.current?.contains(event.target)) { setFocusHeld(lastCue.current); } }; const onFocusOut = (event) => { const node = containerRef.current; if (!node?.contains(event.target)) return; if (node.contains(event.relatedTarget)) return; setFocusHeld(null); }; document.addEventListener('focusin', onFocusIn); document.addEventListener('focusout', onFocusOut); return () => { document.removeEventListener('focusin', onFocusIn); document.removeEventListener('focusout', onFocusOut); }; }, [containerRef]); useEffect(() => { if (!focusHeld) return; if (!containerRef.current?.contains(document.activeElement)) { setFocusHeld(null); } }, [cue, focusHeld, containerRef]); return focusHeld ?? cue; }While thing successful the sheet has focus, the layout freezes. The timeline underneath keeps running, and the infinitesimal attraction leaves, the grid catches up to wherever it sewage to. The clasp thumps an expiring cue and an arriving 1 equally, because collapsing the section and replacing it are the aforesaid problem for personification halfway done typing.
LinkThe compression is older than the web
All of that took an day and a stylesheet. But for funsies, it's worthy knowing what it utilized to take.
Apparently, the hardware could do it by the early eighties. Ampex's ADO and the integer video effects boxes that followed could standard and reposition a unrecorded awesome successful existent time, and they costs capable that the capacity stayed locked up successful post-production, wherever a squeezeback was a prestige effect you budgeted for. Doing 1 unrecorded was uncommon capable to beryllium worthy remarking on.

Then manufacturers folded DVEs into characteristic generators, the container already sitting connected the transmission path, and the compression stopped being a typical effect and became a layout. North American networks started squeezing extremity credits to way the adjacent show successful the precocious nineties, Channel 4 carried it crossed the Atlantic, and the BBC was moving unrecorded promo squeezes by 2000.
Sports sewage location astatine astir the aforesaid clip and past seemingly mislaid liking for six years. TBS put ads beside a unrecorded NASCAR title successful 2000, the format went dormant crossed the adjacent authorities deal, Turner revived it for the Wide Open Daytona broadcasts from 2007 to 2011, ESPN followed pinch NASCAR Nonstop until 2014, and Fox spent 2025 pushing side-by-side crossed its green-flag breaks.
A afloat 20 years passed betwixt imaginable and routine, and the spread only closed erstwhile the effect moved into a instrumentality group already had open. Too lazy? Too pricey? I consciousness for illustration an statement could beryllium made either way.
LinkDon’t extremity there
Every layout successful this station returns to afloat bleed erstwhile its cue ends, but thing requires that. If you time off the past cue open, the video will enactment precisely wherever the compression near it, holding a area of a grid that now has room for a full page-like layout underneath.
css
.stage[data-layout='handoff'] { grid-template-columns: 0fr 30fr 70fr; grid-template-rows: 0fr 30fr 70fr; }The video keeps a 30% area and keeps playing, the bottommost statement opens to afloat width, and that statement tin see afloat merchandise details. Playback remains soft arsenic a baby’s tuchus, since we’re not rubbing the video subordinate astatine all.
You tin ideate each kinds of different usage cases for this treatment:
- A motion connection expert successful a area cell, cued by the VTT truthful it only appears for the segments that request one.
- Slides beside a talk, timed by the aforesaid matter record the video already ships with.
- A 2nd perspective that trades places pinch the main 1 (nothing says the halfway compartment has to beryllium the large one).
The demo codification is on GitHub. If you build thing pinch it, nonstop it our way!
English (US) ·
Indonesian (ID) ·