Morph Transition
An element that really goes from display: flex to display: none — and back — without losing its animation: it shrinks to 0.3, rounds into an oval and fades in 0.6 s before leaving the flow, then comes back the same way thanks to @starting-style. transition-behavior: allow-discrete where the browser can animate display, a measured JavaScript fallback elsewhere (Firefox), a 0.15 s fade under reduced motion.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Transitions has 52 effects, including 9 free. Explore the category →
3 usage examples



How it works
The .morph-tr-element block (pink gradient #ec4899 → #f43f5e, 14 px corners) is driven by a button that points to it with aria-controls. To hide it, one class, .morph-tr-display-none, asks at once for display: none, opacity: 0, scale(0.3) and border-radius: 50%. Normally display flips at once and cuts any animation. Here the transition lists it with allow-discrete (transition: all 0.6s …, display 0.6s allow-discrete): the browser keeps the element displayed for the 0.6 s, plays the shrinking and rounding, and only then removes it. Measured in Chromium 151 and WebKit 26.5: at 100 ms, opacity 0.38 to 0.43 and scale 0.55 to 0.60; at 950 ms, display: none.
Coming back raises another problem: an element leaving display: none has no "before" state, so nothing to animate. The @starting-style rule provides one (opacity 0, scale 0.3, 50% corners): on display, it starts from there and returns to its shape on the cubic-bezier(0.34, 1.56, 0.64, 1) bouncy curve — measured at 100 ms: opacity 0.57 to 0.62, scale 0.70 to 0.73, in all three engines. A consequence to know: @starting-style also plays on the page's first display, so the element eases in on load.
Firefox 153 answers yes to CSS.supports('transition-behavior', 'allow-discrete') but does not animate display yet: without a fallback, the element would vanish at once there (measured: display: none and opacity 0 from 150 ms). The script (2.4 KB) checks it on an invisible test element at load — it sets a div to display: none with the transition and reads whether display held. If it did not, it animates through the .morph-tr-hidden class, then sets .morph-tr-gone (display: none) after the 0.6 s; on entry, it removes .morph-tr-gone, forces a calculation, then removes .morph-tr-hidden. Same result measured in Firefox: opacity 0.39 at 100 ms, display: none at 700 ms. A hidden start is declared with aria-expanded="false" on the button: the script sets the hidden-state classes that suit the browser.
Accessibility
- Truly absent once hidden: with
display: none, the element is neither read by screen readers nor reached by the Tab key — which a mere opacity of 0 does not guarantee. That is the point of this effect for a form field or a filter panel. - Button state: the button carries
aria-controlsandaria-expanded, updated on every click (truewhen the element is shown, measured). For a checkbox that shows a field (scene 1), keep a real button or a real checkbox whose change calls the same toggle. - Reduced motion: under
prefers-reduced-motion, no shrinking or rounding: a 0.15 s fade, thendisplay: none(measured in all three engines: scale 1 and opacity 0.08 to 0.11 at 100 ms, removed at 350 ms). - Contrast: 13.6 px bold white text on #ec4899 → #f43f5e, i.e. 3.5 to 3.7:1, under the 4.5:1 AA threshold. For text that matters, use #be185d → #be123c (6.0 and 6.3:1) or put the content on a light background, as in the scenes.
Browser compatibility
CSS (@starting-style, transition-behavior: allow-discrete, @supports) + 2.4 KB of JavaScript, no dependency. Measured in Chromium 151, WebKit 26.5 (CSS path) and Firefox 153 (JavaScript path): same animated exit followed by display: none, same entrance, same end state, fast double click without a stuck state, two independent elements on the same page. Public markers: @starting-style since Chrome 117, Safari 17.5 and Firefox 129; display transitions since Chrome 117 and Safari 18; Safari 17.4 to 17.6 only knows the allow-discrete value, and the script takes over there.
In older browsers, the script takes the class path: animated exit then display: none, animated entrance. Only the arrival on page load (carried by @starting-style) is not animated there. Without JavaScript, the button does nothing and the element stays in its starting state.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="morph-tr-stage">
<div class="morph-tr-wrapper">
<div class="morph-tr-element" id="morphTrElement">…</div>
</div>
<button class="morph-tr-btn" type="button" aria-controls="morphTrElement" aria-expanded="true">…</button>
</div>
The full HTML + CSS + JS of Morph Transition, ready to paste — plus all 840 effects. Or $299 once, lifetime access.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Exit shape (CSS — .morph-tr-display-none, .morph-tr-hidden and @starting-style) |
scale(0.3), border-radius: 50%, opacity 0 |
translateY(-8px) instead of the scale gives a soberer upward exit. Change the three rules together so entrance and exit match. |
Duration (CSS — 0.6s; JS — 600 in the fallback) |
0.6 s | Keep the same value in the CSS (display 0.6s allow-discrete included) and in the script's delay, otherwise Firefox removes the element too early or too late. |
Freeing the space (CSS — .morph-tr-wrapper and the element's position: absolute) |
fixed 200 × 100 px frame: the space stays reserved | For the following content to move up, put the element back in the flow (position: relative) and remove the frame's height. Do not set a display with a selector stronger than .morph-tr-element.morph-tr-display-none (inline style, id…): the hidden-state rule must be able to set it to none. The less specific display: flex in the sold CSS does no harm. |
Hidden start (HTML — aria-expanded="false" on the button, morph-tr-display-none class on the element) |
shown on load | The class avoids a brief flash before the script runs; the script then sets the right class combination for the browser. |
Entrance on load (CSS — @starting-style block) |
the shown element eases in when the page opens | To have it there from the start, add a class to the page after the first display and limit the @starting-style rule to that class. |
Bounce (CSS — cubic-bezier(.34, 1.56, .64, 1)) |
slight overshoot on entrance | ease for a serious form; the bounce suits a bubble or a card better. |
FAQ
display: none works: during the 0.6 s, the element stays displayed (it shrinks visually, but scale does not change the space it takes), then it is removed and what follows moves up in one go. On entrance it is the reverse: the space opens first, then the element grows. For a gradual slide of the following content, the height has to be animated, as Accordion Fold (fx-0720) does.CSS.supports('transition-behavior', 'allow-discrete') while removing the element without waiting for the end of the transition (measured). The test element checks the real behavior: it sets an invisible div to display: none with the transition and looks whether display still holds. The day Firefox animates display, it will take the CSS path without any code change.@starting-style for the entrance.