One component, one clock

Overlay and dialog share one duration and one curve.

Emil Kowalski@emilkowalski
Made a small dialog animation improvement at Linear: Matched the previously mismatched animation duration of overlay and the dialog + swapped spring animaton for an ease-out one so it feels less abrupt.
Sep 23, 2026Demo 1 ↓Demo 2 ↓View on X ↗

Durations are illustrative. Ranges and curve are from emil-design-eng.

Before and after

Open both, then close with Esc or the dim area.

Two clocksBad
Acme · Settings

scrim 150ms ease · dialog spring(420, 20)
Both on one clockGood
Acme · Settings

scrim + dialog 200ms in · 150ms out
.modal { --modal-duration: 200ms; --modal-ease: cubic-bezier(0.23, 1, 0.32, 1); }
.modal:not([data-open]) { --modal-duration: 150ms; }

.scrim {
  opacity: 0;
  transition: opacity var(--modal-duration) var(--modal-ease);
}
.dialog {
  opacity: 0;
  transform: translateY(8px) scale(0.95);
  transition:
    opacity var(--modal-duration) var(--modal-ease),
    transform var(--modal-duration) var(--modal-ease);
}
.modal[data-open] .scrim { opacity: 1; }
.modal[data-open] .dialog { opacity: 1; transform: none; }

Set the two clocks

Pull the durations apart, press Play, then Match.

One dialog, two layersMismatched
Roadmap · Q4
Δ 150ms
const timing = { duration: 200, easing: "cubic-bezier(0.23, 1, 0.32, 1)", fill: "forwards" };

scrim.animate({ opacity: [0, 1] }, timing);
dialog.animate(
  { opacity: [0, 1], transform: ["translateY(8px) scale(0.95)", "none"] },
  timing,
);

Frame by frame

Drag the scrubber, then switch to Close.

Two clocksBad
Acme · SettingsPreferences
Overlay0% Dialog0%
Both on one clockGood
Acme · SettingsPreferences
Overlay0% Dialog0%
// Before: two clocks
<Overlay transition={{ duration: 0.15 }} />
<Dialog transition={{ type: "spring", stiffness: 420, damping: 20 }} />

// After: one clock
const transition = { duration: 0.2, ease: [0.23, 1, 0.32, 1] };
<Overlay transition={transition} />
<Dialog transition={transition} />

Why it matters, pros and cons

Why it matters

Scrim and panel read as one object; if one ends first, the page looks broken.

Pros

  • Reads as one object, not two.
  • One token to tune, no drift.
  • A known end for focus return and unmount.

Cons

  • Springs have no fixed duration; use a duration-based spring.
  • A 20-30ms stagger is fine if both end together.
  • A dragged sheet's backdrop follows the finger, not a clock.