Shared Element
A single element, never copied, slides from one slot to the other in 0.8 s with a 17 px overshoot before settling, grows by 15% and turns from pink to violet: the object stays the same while its place changes. A CSS transition on transform, between two positions known in advance — 0.6 KB of JavaScript for the toggle, direct arrival when the user reduces 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 .shared-container scene is 300 × 200 px and sets two 110 × 130 px .shared-slots side by side, dashed-bordered, 30 px apart. The shared element .shared-ball (60 px circle, #e11d48 → #f43f5e gradient, pink shadow) belongs to neither: it is absolutely positioned in the scene, 25 px from the left edge. The whole .shared-trigger area is a button (role="button", tabindex="0"): a click, Enter or Space toggles the .active class on the scene and updates aria-pressed. Each area on the page has its own listener; nothing is exposed on window.
With .active, the element gets transform: translateX(190px) scale(1.15) (190 px = 300 px scene − 85 − 25), a violet #7c3aed → #a78bfa gradient and a violet shadow. The 0.8 s transition follows cubic-bezier(.34, 1.56, .64, 1), a curve that goes past 1: measured every 100 ms in Chromium 151 and Firefox 153, the element passes 116, 176, 214 then 228 px, overshoots its place by 17 px around 400-500 ms, comes back and settles at 211 px at 800 ms. The movement goes through transform, which the browser composites without recomputing layout; the shadow changes color during the trip, but the gradient, which does not interpolate, switches at the click. A second click plays the reverse path with the same bounce.
This is a hand-made technique: both positions are known in advance and written in the CSS, the element never changes parent in the DOM. The View Transitions API does something else: it photographs the page before and after a DOM change and interpolates between the two pictures (see View Transitions Page Reveal). Here, no snapshot: the real element moves, with its text, hover states and focus, and it works in every browser that knows CSS transitions. The trade-off: if the destination depends on layout (a card changing lists), positions must be measured in JavaScript, as the FAQ describes.
Accessibility
- Keyboard: the area is a button reachable with Tab; Enter and Space toggle like the click (measured in Chromium and Firefox),
aria-pressedswitches totrue, and a 2 px white outline marks focus. Itsaria-label("Move the element to the other slot") states the action; replace it with yours ("Mark the order as shipped"). - Reduced motion: under
prefers-reduced-motion, the transition is removed and the element is in the other slot by the next frame — measured: 211 px 60 ms after the click, no overshoot. - State change: the movement is seen but not heard. If the slot carries meaning (status, team), also write it in the element's text or announce it in an
aria-livearea: a screen reader only perceivesaria-pressed. - Contrast: white "GO" gives 4.7:1 on red
#e11d48but 3.7:1 on the light pink and 2.7:1 on#a78bfa; keep the text short and bold, or darken the end of the gradients. The slot labels (30% white, 2.6:1) and the bottom hint (3.1:1) are decorative: do not give them information.
Browser compatibility
CSS transitions on transform, box-shadow and background, and about fifteen lines of JavaScript, no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: same 190 px path, same overshoot, same arrival at scale 1.15, same return on the second click; direct arrival under reduced motion. Only the background gradient does not interpolate (no browser animates background-image): it switches at the start.
Without JavaScript, the element stays in the first slot and clicking does nothing. With reduced motion, it changes slot without a path. Without CSS transitions (very old browsers), it jumps from one slot to the other.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="shared-root">
<div class="shared-trigger" role="button" tabindex="0" aria-pressed="false" aria-label="…">
<div class="shared-container">
<div class="shared-slot"><span class="shared-slot-label">Slot A</span></div>
<div class="shared-slot"><span class="shared-slot-label">Slot B</span></div>
<div class="shared-ball">GO</div>
</div>
<span class="shared-hint">Click to toggle</span>
</div>
</div>
The full HTML + CSS + JS of Shared Element, 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 |
|---|---|---|
Path (CSS — translateX(190px)) |
190 px to the right | Computed for the 300 px scene. translateY(-80px) gives a vertical path; translate(190px, 40px) a diagonal. The overshoot is about 9% of the path: leave that margin at the edge. |
Curve and duration (CSS — transition: .8s cubic-bezier(.34, 1.56, .64, 1)) |
0.8 s, overshoot then return | cubic-bezier(.2, .8, .2, 1) arrives without overshooting; 0.5 s for a short move, beyond 1 s the gesture feels slow. |
Growth on arrival (CSS — scale(1.15)) |
×1.15 | scale(1) keeps the size; an element that changes importance (selected, highlighted) can go to 1.3. |
Colors (CSS — .shared-ball background and box-shadow) |
pink #e11d48 → violet #7c3aed |
The shadow glides from one color to the other, the gradient switches at the start. For a color that changes on the way, use a plain background-color (it interpolates). |
Element shape (CSS — .shared-ball) |
60 px circle | A card, a badge, an image: change width, height and border-radius, then recompute the path so it lands in the second slot. |
FAQ
document.startViewTransition, view-transition-name) photographs the page before and after a DOM change and animates between the two pictures; that is what View Transitions Page Reveal (fx-0786) does. Shared Element animates the real element with a CSS transition on transform, between two positions written in the stylesheet. Advantages: it works everywhere, keeps text selectable and focus during the trip. Limit: the element does not change parent, positions must be known.var first = el.getBoundingClientRect()), move it in the DOM (columnB.appendChild(el)), measure the new one (last), apply without transition el.style.transform = 'translate(' + (first.left - last.left) + 'px, ' + (first.top - last.top) + 'px)' to put it visually back at its starting point, then, on the next frame (requestAnimationFrame), reset transform to zero with this effect's transition. The element slides to its new place, with the same overshoot.background-image), and browsers do not interpolate one gradient into another: the value switches at the start of the transition. The shadow, however, is a plain color and goes from pink to violet during the whole trip. For a color that changes on the way, replace the gradient with background-color, or layer a violet ::before pseudo-element whose opacity goes from 0 to 1.