Gallery Flow Morph
Six scattered tiles that drift and follow the mouse; a click turns a tile into a full-frame view, and Escape sends it back to its place.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Gallery has 44 effects, including 6 free. Explore the category →
3 usage examples



How it works
Inside .gfm-scene (16/10 format, 560 px wide at most), the six .gfm-tile buttons are placed in percentages and rise or sink a few pixels (a 7 s back-and-forth gfm-drift animation). When the pointer moves over the scene, the script sets --px and --py (position from -0.5 to 0.5) and each tile shifts by up to 14 px in the direction given by its --dx / --dy variables: a light parallax, different for each tile.
A click (or Enter) on a tile opens the .gfm-viewer: for the duration of the transition, the tile and the large .gfm-hero image share one view-transition-name, and the View Transitions API animates one into the other in 0.45 s. The title (data-title) appears letter by letter under the image. ✕, a click on the dark background or Escape close it with the reverse movement and return focus to the original tile. Without View Transitions, the viewer fades in and out (0.3 s and 0.25 s).
Tiles accept your images: an <img> in a tile covers it, and the viewer shows the same image large, or the version named in data-full if you provide a heavier one. Without an image, the --art gradient is the visual, as in the demo. Each .fx-0819-root on the page has its own gallery.
Accessibility
- Tiles by keyboard: they are
<button>s named by their text, with a violet:focus-visibleoutline and the same slight zoom as on hover. - Opening and closing: on open, focus goes to ✕ ("Close full-screen view") and the tiles behind become
inert; Escape or ✕ close it and focus returns to the clicked tile. - Title read in one piece: the animated letters are hidden from screen readers (
aria-hidden) and the full title is announced through an invisible copy in thearia-live="polite"caption. - Reduced motion: under
prefers-reduced-motion: reduce, no drift, no parallax, no morph or fade, no letter-by-letter reveal: the viewer opens and closes directly. - Drift pause: a round button at the bottom left (
.gfm-pause, "Pause the drift",aria-pressed="true"while it is stopped) stops and restarts the drift by click, touch and keyboard; the drift also pauses while the gallery is hovered and while keyboard focus is inside it. Under reduced motion there is no drift and the button is hidden. Escape only closes the gallery that has focus.
Browser compatibility
document.startViewTransition: Chrome 111, Safari 18, Firefox 144 (according to MDN); elsewhere, a CSS fade. The translate and scale properties (Chrome 104, Firefox 72, Safari 14.1) and inert (Chrome 102, Firefox 112, Safari 15.5). Measured in Chromium 151, Firefox 153 and WebKit 26.5 (Playwright): two galleries with only the second one driven, opening by keyboard, closing with Escape and focus back on the tile, reduced motion, 390 px without overflow, 0 errors.
Without View Transitions the viewer fades in and out. Without JavaScript the tiles show and drift; neither clicking nor the pause button does anything.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0819-root">
<div class="gfm-scene">
<div class="gfm-field">
<button class="gfm-tile" type="button" data-title="…" style="--dx: -1; --dy: -0.6"><img src="…" alt=""><span class="gfm-tile-name">…</span></button>
… (6 tiles)
</div>
<button class="gfm-pause" type="button" aria-pressed="false" aria-label="…"></button>
<div class="gfm-viewer" hidden>
<figure class="gfm-hero"></figure>
<p class="gfm-caption" aria-live="polite"></p>
<button class="gfm-close" type="button" aria-label="…">✕</button>
</div>
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Images (HTML — <img> in .gfm-tile) |
none: --art gradients |
The image covers the tile and shows large in the viewer. Keep alt="" if the tile's name is enough. |
Large version (HTML — data-full on .gfm-tile) |
the tile's own image | URL of a more detailed image, loaded only on opening. |
Title (HTML — data-title) |
Aurora, Magma, Ocean… | Text shown letter by letter under the image and read by screen readers. |
Drift and parallax (HTML — --dx / --dy in the tile's style) |
from -1.2 to 1.1 depending on the tile | Direction and strength of the mouse shift (14 px × value) and drift offset. |
Pause button (HTML — .gfm-pause) |
shown at the bottom left, aria-pressed="false" |
Stops or restarts the drift; change its aria-label when you translate. Only remove it if you also remove the drift. |
Maximum width (CSS — --gfm-max-width) |
560px | The scene takes its container's width up to this limit; none for the full width. |
Morph duration (CSS — ::view-transition-old/new(gfm-active)) |
.45s | Speed of the tile → full-frame move. |
Tile positions (CSS — .gfm-tile:nth-child(n)) |
6 positions in % | left / top coordinates and width of each tile; adjust them if you use more or fewer. |
FAQ
.gfm-tile:nth-child(1) to (6)). A 7th tile would land at the top left over the first one: add a :nth-child(7) rule with its place.gfm-paused class on .gfm-scene), hover and keyboard focus pause it too, and it disappears under reduced motion. To remove it everywhere, delete the animation: gfm-drift … line from .gfm-tile and the .gfm-pause button: the mouse parallax stays.