Spotlight's Liquid Glass Morph, Frame by Frame

A measured teardown of the macOS Tahoe Spotlight enter/exit animation, from a 57 fps screen recording — so we can clone it faithfully in Script Kit.

Spotlight bar, steady state
Source: CleanShot 2026-07-20 18.18.15.mp4 36 frames @ 57 fps (17.5 ms/frame) Steady bar: 485 × 119 px (measured)

Method

Frame 1 contains no Spotlight — a clean background plate. Every other frame is background-subtracted against it, so the only differing pixels belong to Spotlight itself. Per frame, three measurements were extracted:

1. Edges — column/row intensity profiles of the diff, smoothed, with edges at threshold crossings (per-frame–relative for bright frames, absolute-from-steady for cross-checks). 2. Presence — mean diff intensity inside the fixed steady-state bar rectangle, normalized so steady = 1.0. This is an opacity proxy (during the exit it conflates fade with blur, both of which remove energy from the region — which is exactly what your eye sees). 3. Visual keyframes — cropped strips for qualitative confirmation.

Caveat: below ~50% presence the edge measurements drown in video-compression noise, so size numbers in the faintest frames are extrapolated from the trend, and are labeled as such.

The Enter: fade in wide → squish under → rebound out

Entry keyframes: frames 2, 4, 5, 7, 9, 19
Frames 2, 4, 5, 7, 9, 19 (top to bottom): materializing faint-and-wide, sharpening while shrinking, settled.
~140 ms
fade-in, symmetric S-curve
−1.3%
width undershoot below final
~300 ms
total, first photon → settled
~0%
height participation in the squish
Enter width vs time chart
Enter opacity vs time chart

Measured timeline (t=0 at first visible frame)

t (ms)width (% final)opacityevent
0~110 (extrap.)0.05first photon — faint, wide
35~105 (extrap.)0.36materializing
53104.70.55first clean width reading
70102.50.71shrinking, decelerating
88101.20.85
105100.00.95crosses natural width — keeps going
12399.00.99fade complete as it compresses
15898.70.99max squish (−1.3%)
21099.21.00rebounding out
26499.61.00
~300100.01.00settled

What the numbers say

It's released elastic, and the timing is asymmetric. The compression stroke (wide → max squish) takes ~140 ms; the rebound (squish → natural) takes ~140 ms more but travels only a quarter of the distance — so the rebound feels much slower and softer. The window is effectively at rest at max compression: velocity passes through zero there, which is what makes it read as physics instead of keyframes.

The fade and the compression are phase-locked. Opacity completes (≥0.99) at almost exactly the moment width bottoms out. The bar is never fully opaque while still larger than its natural size.

The morph is width-dominant. Height reaches its final value by ~120 ms and does not undershoot measurably (±0–2 px), while width undershoots by ~6 px. The "squish" your eye reads is horizontal.

The Exit: dematerialize — fade + blur + slight growth

Exit keyframes: frames 29 to 34
Frames 29–34 (top to bottom, 17.5 ms apart): caret drops, then the bar grows slightly while a gaussian blur ramps and opacity falls. Gone in ~120 ms.
~120 ms
full exit duration
+3–4%
per-side growth by mid-fade
0 → heavy
gaussian blur ramp
none
undershoot — pure release
Exit presence vs time chart

Measured presence (opacity × blur energy)

t (ms)presencenote
01.00steady
180.92caret gone; growth + blur begin
350.80
530.63clearly blurred
700.38steepest drop
880.24smudge
1050.13
1230.04gone

The presence curve is a symmetric S (slow-fast-slow), and the visual signature is unmistakable: this is a dematerialize, not a shrink. The bar releases outward (the inverse of its entry direction — it entered by shrinking in, it leaves by relaxing back out), while a gaussian blur dissolves the edges and the whole thing fades. No bounce on the way out.

Porting this to Script Kit

Enter — current implementation vs. measured Spotlight

parameterours (now)Spotlight (measured)verdict
start outset / side2%~3–5% (width)slightly larger start is authentic
squish depth1.5%1.3%match
fade windowphase 1 (~160 ms)~140 ms, done at max squishmatch
phase split62% / 38% of 260 ms~50% / 50% of ~280 mslengthen the rebound
axis biasuniform scalewidth-dominant (height ~static)optional: damp height delta to ~40%
// Recommended knob changes (theme defaults)
GLASS_MORPH_DEFAULT_DURATION = 0.28   // was 0.26
GLASS_MORPH_DEFAULT_INSET    = 0.03   // was 0.02 (per side)
// phase split in animate_tahoe_glass_appearance:
phase1 = duration * 0.50              // was 0.62 — equal strokes,
                                      // rebound covers 1/4 the distance
                                      // so it reads twice as gentle

Exit — new design (we currently hard-cut)

Three simultaneous channels over ~120 ms, all ease-in-out:

// 1. alpha: 1 → 0            (window animator, 0.12 s)
// 2. frame: grow ~3% per side (the inverse release of the entry)
// 3. blur : CAFilter "gaussianBlur" on contentView.layer,
//           inputRadius 0 → ~8pt   ← same private-API pattern the
//           repo already uses for backdrop saturation
// then orderOut.
Integration hazard, already learned the hard way: the native orderOut: must stay synchronous inside the platform hide path — deferring it there livelocked the hotkey gesture listener. The exit animation has to run above that layer: play the 120 ms dematerialize first, then invoke the normal hide flow. The dormant animate_tahoe_glass_disappearance is the starting point; add the blur channel and move its call site up the stack.

Appendix: full measurement dump

Width ratios are only reported where presence > 0.5 (below that, compression noise dominates the edges). Frames 20–29 are omitted — all identical to steady state within ±0.4%.

framet (ms)w (px)w/finalpresencephase
100.00absent
217.50.05enter
335.10.19
452.60.36
570.25081.0470.55
687.74971.0250.71
7105.34911.0120.85
8122.84851.0000.95
9140.44800.9900.99
10157.94790.9880.99squish + rebound
11175.44780.9860.99
13210.54810.9921.00
16263.24820.9961.00
18298.24841.0001.00
30508.80.92exit
31526.30.80
32543.90.63
33561.40.38
34578.90.24
35596.50.13
36614.00.04

Measured with background subtraction + intensity-profile edge detection (numpy/PIL) from a 1588×1062 @ 57 fps CleanShot recording, 2026-07-20.