Two related things, both landing in GlassDisclosurePanel.
The dropdown bounce now matches the cards
EASING_BOUNCY is a full spring: roughly thirty linear() control points that
overshoot to 1.337, dip to 0.895, rise to 1.033 and ring down through
several more oscillations before settling. It is built for TIMING_BOUNCY
(750ms), which is what ContentCard gives it — that is why the cards feel good.
The header panel ran it over TIMING_SLOW (300ms). Compressing that many
oscillations into 300ms is what read as stutter rather than bounce.
I confirmed with computed styles what was actually winning before changing
anything. The panel spread createBouncyTransition(['opacity','transform'], TIMING_SLOW) and then overwrote the transition key on the next line, so there
was a shorthand declaring ease-out plus a longhand declaring the spring.
The longhand landed after the shorthand, so the effective style was
transition-property: opacity, transform, visibility at
0.3s, 0.3s, 0s with the full spring on all three. So this is not a swap
from ease-out to a spring — the spring was already running, just far too fast,
and on opacity too.
Now:
| property | duration | easing |
|---|---|---|
transform |
TIMING_BOUNCY (750ms) |
EASING_BOUNCY |
opacity |
TIMING_NORMAL (150ms) |
ease-out |
visibility |
0s, delayed TIMING_NORMAL on close |
— |
The curve itself is unchanged. The transform runs the same spring over the
same duration as ContentCard, so the two are the same motion rather than
merely similar. It does not feel sluggish despite 750ms because the panel only
travels 12px: the spring is within 3% of its target by ~47% of the way through,
which is about ±1px here, so everything after the first ~200ms is imperceptible
settle. Opacity is deliberately off the spring — it clamps at 1, so an overshoot
flattens instead of springing.
The visibility delay tracks the fade rather than the transform. Tying it
to 750ms would leave the panel painted and in the a11y tree for 600ms after it
had already faded to nothing.
REDUCED_MOTION still wins: Emotion emits the nested media block after the flat
declarations, and transition: none resets the whole shorthand. Verified under
emulated prefers-reduced-motion: reduce.
A helper bug that made the above impossible
createBouncyTransition returned a transition shorthand plus a bare
transition-timing-function longhand. That longhand carries no property list of
its own, so it restyles every entry of whatever shorthand precedes it. Since
GlassContainer spreads this helper into its base styles, every glass surface
carried a stray spring longhand, and per-property easing on a glass element was
silently impossible — my first attempt at the fix was quietly overridden by it.
It now emits a single shorthand. No caller passed the third easing argument and
no caller set transition after the spread, so the effective easing is unchanged
everywhere; verified ContentCard, its overlay, the capsule glass and the trigger
hover scale all still compute the spring at their previous durations.
One dropdown presentation on the site
GlassSwitcher's mobile branch was a raw MUI Menu with MenuItems — a flat
opaque panel that looked nothing like the header menus. It is deleted, not
restyled, and the branch now renders the same GlassDisclosurePanel behind the
music and theme menus. That makes the panel's motion work visible in a third
place.
Notes on that:
- Anchoring. The panel is pinned to the trigger's inline start here via a
newalignprop. The header pins to the inline end because its capsule is at
the right edge; the sorter sits at the page's left edge, where inline-end
anchoring would have put a 208px panel atleft: -128px, off screen. - Glass. The disc is an absolutely positioned sibling behind the trigger, not
a parent, for the same reason the capsule is:backdrop-filterwould make it
the backdrop root for a panel that hangs below it with nothing behind to
sample, painting it transparent. - Selection semantics stay a native radio group. The two breakpoint branches
both render (CSS picks), so the mobile group gets its ownnamerather than
merging with the desktop row. - Text-only rows. Sort options have no icons, so
GlassDisclosureRow's icon
is now optional and such rows collapse to one full-width column instead of
indenting past an empty 48px cell. I did not invent icons for the sort options. - No tooltip on the mobile trigger — there wasn't one before, it only shows at
xswhere there's no hover, andaria-labelnames it. The desktop options'
tooltips and their:focus-visibleguard are untouched. - The selected-row thumb was duplicated between the theme picker and this, so it
is now a sharedGlassDisclosureThumb.
The desktop inline segmented control is deliberately unchanged — that's a
different control shape, not the thing in the screenshot.
Verification
Recorded both header menus and the mobile sorter opening and closing, light and
dark, at 390 and 1280, with a content-card hover in the same clip so the bounce
is directly comparable.
Checked by hand on /music/albums, where the panel hangs off a sticky bar:
- Not clipped on any edge at 390, at the top of the page or scrolled 700px down;
every row stays hit-testable above the scrolling grid. - The sticky ancestor has
overflow: visibleand the panel gets its own
backdrop-filter, sohtml, body { overflow-x: clip }doesn't bite. - No persistent
view-transition-nameadded to the panel or its trigger. The
header disc stays at full opacity through an album open and close, and the
capsule glass still has zero children. - Opening a header menu closes the sorter and vice versa; no simultaneous panels.
- Console is clean through the whole sequence.
turbo check, turbo check:types and turbo test all pass — 347 tests across
5 packages.