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.

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.
The Enter: fade in wide → squish under → rebound out


Measured timeline (t=0 at first visible frame)
| t (ms) | width (% final) | opacity | event |
|---|---|---|---|
| 0 | ~110 (extrap.) | 0.05 | first photon — faint, wide |
| 35 | ~105 (extrap.) | 0.36 | materializing |
| 53 | 104.7 | 0.55 | first clean width reading |
| 70 | 102.5 | 0.71 | shrinking, decelerating |
| 88 | 101.2 | 0.85 | |
| 105 | 100.0 | 0.95 | crosses natural width — keeps going |
| 123 | 99.0 | 0.99 | fade complete as it compresses |
| 158 | 98.7 | 0.99 | max squish (−1.3%) |
| 210 | 99.2 | 1.00 | rebounding out |
| 264 | 99.6 | 1.00 | |
| ~300 | 100.0 | 1.00 | settled |
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

Measured presence (opacity × blur energy)
| t (ms) | presence | note |
|---|---|---|
| 0 | 1.00 | steady |
| 18 | 0.92 | caret gone; growth + blur begin |
| 35 | 0.80 | |
| 53 | 0.63 | clearly blurred |
| 70 | 0.38 | steepest drop |
| 88 | 0.24 | smudge |
| 105 | 0.13 | |
| 123 | 0.04 | gone |
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
| parameter | ours (now) | Spotlight (measured) | verdict |
|---|---|---|---|
| start outset / side | 2% | ~3–5% (width) | slightly larger start is authentic |
| squish depth | 1.5% | 1.3% | match |
| fade window | phase 1 (~160 ms) | ~140 ms, done at max squish | match |
| phase split | 62% / 38% of 260 ms | ~50% / 50% of ~280 ms | lengthen the rebound |
| axis bias | uniform scale | width-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.
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%.
| frame | t (ms) | w (px) | w/final | presence | phase |
|---|---|---|---|---|---|
| 1 | 0 | — | — | 0.00 | absent |
| 2 | 17.5 | — | — | 0.05 | enter |
| 3 | 35.1 | — | — | 0.19 | |
| 4 | 52.6 | — | — | 0.36 | |
| 5 | 70.2 | 508 | 1.047 | 0.55 | |
| 6 | 87.7 | 497 | 1.025 | 0.71 | |
| 7 | 105.3 | 491 | 1.012 | 0.85 | |
| 8 | 122.8 | 485 | 1.000 | 0.95 | |
| 9 | 140.4 | 480 | 0.990 | 0.99 | |
| 10 | 157.9 | 479 | 0.988 | 0.99 | squish + rebound |
| 11 | 175.4 | 478 | 0.986 | 0.99 | |
| 13 | 210.5 | 481 | 0.992 | 1.00 | |
| 16 | 263.2 | 482 | 0.996 | 1.00 | |
| 18 | 298.2 | 484 | 1.000 | 1.00 | |
| 30 | 508.8 | — | — | 0.92 | exit |
| 31 | 526.3 | — | — | 0.80 | |
| 32 | 543.9 | — | — | 0.63 | |
| 33 | 561.4 | — | — | 0.38 | |
| 34 | 578.9 | — | — | 0.24 | |
| 35 | 596.5 | — | — | 0.13 | |
| 36 | 614.0 | — | — | 0.04 |
Measured with background subtraction + intensity-profile edge detection (numpy/PIL) from a 1588×1062 @ 57 fps CleanShot recording, 2026-07-20.