npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-motion-gallery

v2.0.47

Published

Composable React media gallery primitives for sliders, grids, masonry, fullscreen, video, zoom/pan, and stable skeleton loading

Readme

React Motion Gallery

Composable React media gallery primitives for production interfaces: sliders, grids, masonry, structured entries, fullscreen, thumbnails, video, zoom/pan, and loading states that are designed around the layout they protect.

The package stays close to React composition. Slider, Grid, and Masonry render children directly; Entries renders structured data; GalleryCore coordinates fullscreen state; Video handles Plyr-backed media; ZoomPanImage gives you a standalone zoom surface; and Skeleton can be used inside or outside gallery layouts. For loading-state precision, the repo also includes a development-time browser measurement workflow that turns real rendered text into stable skeleton text authoring data, including reflow-sensitive layouts such as masonry.

Runtime Gzip Sizes

This table reports local gzip measurements for selected runtime surfaces. Type-only imports are erased and add no JS; feature subpath rows measure only that feature entry point. The script rebundles one export at a time from its published ESM entry point, excludes peer and runtime externals, and gzips the resulting JS bundle. Run npm run build && npm run size:readme in packages/react-motion-gallery to refresh it.

| Surface | JS gzip | | --- | --- | | Entries | 15.7kB | | FullscreenThumbnailSlider | 20.3kB | | GalleryCore | 2.6kB | | Grid | 6.3kB | | grid/ready | 323.0B | | grid/lazy-load | 3.3kB | | Masonry | 6.5kB | | masonry/ready | 323.0B | | masonry/lazy-load | 3.3kB | | Skeleton base | 10.7kB | | skeleton/slider | 19.3kB | | skeleton/grid | 13.0kB | | skeleton/masonry | 21.9kB | | Slider core | 18.7kB | | slider/ready | 894.0B | | slider/arrows | 1.2kB | | slider/dots | 933.0B | | slider/progress | 892.0B | | slider/scrollbar | 1.2kB | | slider/auto-height | 1.3kB | | slider/lazy-load | 3.9kB | | slider/parallax | 1.4kB | | slider/scale | 1.2kB | | slider/fade | 1.2kB | | slider/crossfade | 2.8kB | | slider/fullscreen | 959.0B | | ThumbnailSlider | 18.9kB | | useFullscreenController | 5.0kB | | fullscreen/slider | 37.7kB | | fullscreen/controls | 173.0B | | fullscreen/captions | 13.1kB | | fullscreen/zoom-pan | 9.9kB | | fullscreen/video | 16.4kB | | fullscreen/lazy-load | 13.2kB | | fullscreen/crossfade | 181.0B | | fullscreen/thumbnails | 160.0B | | Video | 12.7kB | | ZoomPanImage | 8.7kB | | media / toMediaItems | 260.0B | | responsive / BREAKPOINT_MAP | 85.0B |

Installation

Install the package, then add the optional video peers only if you use Video.

npm install react-motion-gallery
npm install plyr plyr-react

Import the stylesheet. The package uses CSS Modules internally, but consumers only load the compiled plain CSS file, so no CSS Modules setup is required in your app.

import "react-motion-gallery/styles.css";

Overview

Mental model:

  • Slider, Grid, and Masonry render React children directly.
  • Entries renders structured entry data with a custom media container.
  • GalleryCore and useFullscreenController power fullscreen behavior.
  • Video is the gallery-ready video primitive.
  • ZoomPanImage attaches click-to-zoom, drag pan, ctrl-wheel pinch, and touch pinch to one clipped image surface.
  • Skeleton renders standalone placeholders or wraps real content with shared loading-layer timing.

MediaItem accepts three shapes:

  • image: { kind: "image", src, alt?, caption?, srcSet?, sizes?, width?, height? }
  • video: { kind: "video", src, poster?, alt?, caption? }
  • node: { kind: "node", node }

toMediaItems() accepts string URLs, image/video objects, and node objects, then normalizes them into MediaItem[]. String URLs infer kind from the file extension.

import "react-motion-gallery/styles.css";
import { toMediaItems, type MediaItem } from "react-motion-gallery/media";
import { Slider } from "react-motion-gallery/slider";

const items: MediaItem[] = toMediaItems([
  "https://picsum.photos/id/1015/1600/900",
  { src: "https://picsum.photos/id/1018/1600/900", alt: "Mountains" },
  { kind: "node", node: <div>Custom slide</div> },
]);

export function QuickStart() {
  return (
    <Slider>
      {items.map((item, index) =>
        item.kind === "image" ? (
          <img
            key={item.src}
            src={item.src}
            alt={item.alt ?? `Slide ${index + 1}`}
            style={{ width: "100%", aspectRatio: "16 / 9", objectFit: "cover" }}
          />
        ) : item.kind === "node" ? (
          <div key={index}>{item.node}</div>
        ) : null
      )}
    </Slider>
  );
}

Responsive numeric props in this package accept either a plain number or a breakpoint map like { 0: 1, md: 2, 1200: 3 }. Named breakpoints resolve from the internal map: xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536.

The package root exports the primary public components, helper functions, and companion prop types. Use it when one module needs several gallery surfaces. Prefer subpaths for routes or components that only need one surface, such as react-motion-gallery/media or react-motion-gallery/slider.

Subpaths give bundlers a smaller graph than the root. Less JS to transfer, parse, evaluate, and hydrate can improve first loads, cache misses, slower devices, and perceived speed.

| Entry point | Main surface | | --- | --- | | react-motion-gallery/media | toMediaItems, MediaItem, MediaInput | | react-motion-gallery/responsive | BREAKPOINT_MAP and responsive value types | | react-motion-gallery/core | GalleryCore, GalleryCoreProvider, useGalleryCore | | react-motion-gallery/slider | Slider, createSliderIndexChannel, slider types | | react-motion-gallery/slider/ready | useSliderReady | | react-motion-gallery/slider/arrows | sliderArrows | | react-motion-gallery/slider/dots | sliderDots | | react-motion-gallery/slider/progress | sliderProgress | | react-motion-gallery/slider/scrollbar | sliderScrollbar | | react-motion-gallery/slider/ripple | sliderRipple | | react-motion-gallery/slider/auto-play | sliderAutoPlay | | react-motion-gallery/slider/auto-scroll | sliderAutoScroll | | react-motion-gallery/slider/auto-height | sliderAutoHeight | | react-motion-gallery/slider/lazy-load | sliderLazyLoad | | react-motion-gallery/slider/parallax | sliderParallax | | react-motion-gallery/slider/scale | sliderScale | | react-motion-gallery/slider/fade | sliderFade | | react-motion-gallery/slider/crossfade | sliderCrossfade | | react-motion-gallery/slider/fullscreen | sliderFullscreen | | react-motion-gallery/slider/loading | sliderLoading | | react-motion-gallery/grid | Grid, Grid.Item, grid types | | react-motion-gallery/grid/ready | useGridReady | | react-motion-gallery/grid/lazy-load | gridLazyLoad | | react-motion-gallery/masonry | Masonry, Masonry.Item, masonry types | | react-motion-gallery/masonry/ready | useMasonryReady | | react-motion-gallery/masonry/lazy-load | masonryLazyLoad | | react-motion-gallery/entries | Entries, flattenEntries, entry media container helpers | | react-motion-gallery/skeleton/base | Standalone Skeleton and generic skeleton authoring types | | react-motion-gallery/skeleton/slider | SliderSkeleton and slider skeleton authoring types | | react-motion-gallery/skeleton/grid | GridSkeleton and grid skeleton authoring types | | react-motion-gallery/skeleton/masonry | MasonrySkeleton and masonry skeleton authoring types | | react-motion-gallery/skeleton/cache | Server-safe skeleton cookie cache helpers and types | | react-motion-gallery/skeleton/cache/provider | Client SkeletonCacheProvider for SSR snapshots | | react-motion-gallery/fullscreen | useFullscreenController and fullscreen types | | react-motion-gallery/fullscreen/slider | fullscreenSlider | | react-motion-gallery/fullscreen/controls | fullscreenControls | | react-motion-gallery/fullscreen/captions | fullscreenCaptions | | react-motion-gallery/fullscreen/zoom-pan | fullscreenZoomPan | | react-motion-gallery/fullscreen/video | fullscreenVideo | | react-motion-gallery/fullscreen/lazy-load | fullscreenLazyLoad | | react-motion-gallery/fullscreen/crossfade | fullscreenCrossfade | | react-motion-gallery/fullscreen/thumbnails | fullscreenThumbnails | | react-motion-gallery/thumbnails | ThumbnailSlider, thumbnail sync helpers | | react-motion-gallery/fullscreenThumbnails | FullscreenThumbnailSlider | | react-motion-gallery/video | Video and optional Plyr-backed video types | | react-motion-gallery/zoomPan | ZoomPanImage and zoom/pan types |

MCP server

This repository includes react-motion-gallery-mcp, a local Model Context Protocol server for AI-assisted gallery design and integration. It runs over stdio and gives MCP-capable clients a structured way to inspect React Motion Gallery patterns, generate starter components, audit installs, and scaffold skeleton text measurement manifests.

From a local checkout, build the server first:

npm install
npm run build --workspace packages/react-motion-gallery-mcp

Then add it to your MCP client config. Replace the path with the absolute path to your checkout:

{
  "mcpServers": {
    "react-motion-gallery": {
      "command": "node",
      "args": [
        "/absolute/path/to/react-motion-gallery/packages/react-motion-gallery-mcp/dist/server.js"
      ]
    }
  }
}

Once connected, start with workflow classification. The MCP server treats requests as layout intent plus loading fidelity, so agents can avoid unnecessary skeleton work when the user only asked for a layout.

{
  "goal": "Build a pricing card grid with simple skeleton loading",
  "hasExistingLayout": false,
  "layoutHint": "grid",
  "framework": "next"
}

The classifier returns one of these modes:

User goal: "Build a responsive gallery slider."
Workflow: layoutOnly
Use: recommend_pattern -> get_demo -> generate_gallery_component
Skip: skeleton tools
User goal: "Build a product grid with image placeholders while loading."
Workflow: layoutWithNonTextSkeleton
Use: Skeleton rect/media nodes or gallery skeleton wrappers
Skip: browser text measurement
User goal: "Build a card layout with simple text skeleton lines."
Workflow: layoutWithHandAuthoredTextSkeleton
Use: text skeleton nodes with hand-authored lines/barWidth values
Skip: generated sidecar
User goal: "Build a masonry layout where skeleton text matches real responsive copy."
Workflow: layoutWithBrowserMeasuredTextSkeleton
Use: stable selectors -> scaffold_skeleton_text -> generate:skeleton-text-module --analysis-output -> import sidecar

When a connected agent needs context, it should read rmg://context/agent-brief, then use targeted resources such as rmg://guides/layout-selection, rmg://guides/loading-fidelity, rmg://guides/browser-measured-skeletons, rmg://docs, rmg://catalog/demos, and rmg://examples/{demoId}.

Read a specific example:

rmg://examples/slider-video-html5

Call recommend_pattern with your UI goal to choose the right layout, imports, demos, and gotchas.

{
  "goal": "Responsive masonry gallery with lazy-loaded images and fullscreen preview",
  "layout": "masonry",
  "features": ["lazy-load", "fullscreen"],
  "mediaKinds": ["image"],
  "framework": "next"
}

Call classify_gallery_workflow when the user goal is ambiguous about loading fidelity.

{
  "goal": "Add a skeleton that matches the real responsive card copy",
  "hasExistingLayout": true,
  "layoutHint": "custom",
  "framework": "next"
}

Call search_demos to find matching examples by category, tags, component, media kind, or query.

{
  "category": "slider",
  "mediaKind": "video",
  "query": "html5",
  "limit": 3
}

Call get_demo to retrieve consumer-ready TSX/CSS for a specific demo.

{
  "demoId": "slider-video-html5",
  "includeExtraFiles": true
}

Call audit_project with a projectRoot to check installs, stylesheet imports, optional video peers, and common Next.js client-component issues.

{
  "projectRoot": "/absolute/path/to/your-app"
}

Call generate_gallery_component to turn a selected demo into renamed TSX/CSS output for your app.

{
  "demoId": "masonry-balanced",
  "componentName": "ProjectGallery",
  "cssModuleName": "ProjectGallery.module.css"
}

Call write_gallery_files after reviewing generated output. Pass apply: true only when you want the server to write files under projectRoot.

{
  "projectRoot": "/absolute/path/to/your-app",
  "demoId": "masonry-balanced",
  "componentName": "ProjectGallery",
  "componentPath": "src/components/ProjectGallery.tsx",
  "cssPath": "src/components/ProjectGallery.module.css",
  "apply": true
}

Call scaffold_skeleton_text to create a browser-measurement manifest for the skeleton text workflow.

{
  "projectRoot": "/absolute/path/to/app",
  "manifestPath": "src/components/pricing.skeleton-text.browser.manifest.json",
  "url": "http://127.0.0.1:3000/pricing?skeletonMeasure=content",
  "outputFile": "src/components/pricing.skeleton-text.generated.ts",
  "moduleExportName": "pricingSkeletonText",
  "barWidthUnit": "px",
  "includeTextMetrics": true,
  "targets": [
    {
      "exportName": "pricingCardTitle",
      "selector": "[data-skeleton-text-id='pricingCardTitle']"
    }
  ],
  "apply": true
}

Use flat targets for ordinary DOM text in any layout: sliders, grids, masonry cards, entries, thumbnails, flex layouts, app shells, pricing cards, and custom UI. Add the optional slider, masonry, or entries manifest blocks only when those specialized layouts need canonical item measurement, geometry readiness, or row readiness.

The file-writing tools default to dry runs unless apply: true is passed, and they refuse to write outside the provided projectRoot.

Acknowledgements

React Motion Gallery's slider engine includes portions of code derived from Embla Carousel, which is MIT licensed. Those portions have been substantially adapted for React Motion Gallery's React architecture, public API, transition system, fullscreen integration, loading layers, and media workflows.

See THIRD_PARTY_NOTICES.md for the preserved Embla Carousel copyright and MIT license notice.

Core

GalleryCore is the shared state boundary for fullscreen-aware galleries. Wrap a layout in it when you need shared breakpoints, a normalized fullscreen media list, fullscreen-open state, or programmatic fullscreen opening. useGalleryCore() is the public hook for reading that core state from descendants.

GalleryCore props

| Option | Type | Default | Notes | | --- | --- | --- | --- | | children | React.ReactNode | | The gallery tree using the shared core. | | layout | "slider" \| "grid" \| "masonry" \| "entries" | | Declares the owning base layout. Omit it for standalone fullscreen/core usage. | | breakpoints | Record<string, number> | xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536 | Breakpoint map shared with descendants. | | fullscreenItems | MediaItem[] \| string[] | [] | Normalized fullscreen media list. | | nodes | ReactNode \| ReactNode[] | | Advanced initial node list used by the slider-backed imperative state. |

useGalleryCore API

GalleryApi is the public alias for GalleryCoreApi. It covers core fullscreen state and programmatic fullscreen opening. Slider item mutation lives on SliderHandle and SliderApi.

| Field / Method | Type | Notes | | --- | --- | --- | | layout | "slider" \| "grid" \| "masonry" \| "entries" \| null | Current owning layout, or null for standalone fullscreen/core usage. | | effectiveBreakpoints | Record<string, number> | Breakpoint map after merging custom GalleryCore.breakpoints with defaults. | | normalizedItems | MediaItem[] | Fullscreen item list normalized from fullscreenItems. | | fsEnabled | boolean | true when a mounted fullscreen controller has enabled fullscreen behavior. | | setFsEnabled | (enabled: boolean) => void | Enables or disables fullscreen behavior. Usually handled by useFullscreenController. | | isFullscreenOpen | boolean | true while fullscreen is open. | | isFullscreenOpenRef | React.RefObject<boolean> | Ref mirror for handlers that need the current fullscreen-open state. | | setFullscreenOpen | (open: boolean) => void | Updates fullscreen-open state. Usually handled by the fullscreen runtime. | | openFullscreenAt | ({ index, method?, event? }) => void | Opens fullscreen at a normalized fullscreen item index. Pass the source event for scale-origin detection. | | notifyBaseVisibleIndex | (index: number) => void | Emits the visible base media index for fullscreen lazy-load/prewarm coordination. | | notifyFsVisibleIndex | (index: number) => void | Emits the active fullscreen index back to base media. | | registerExpandableImage | (index: number, node: HTMLElement \| null) => void | Registers an origin surface for layoutless scale transitions. |

ZoomPanImage

import { ZoomPanImage } from "react-motion-gallery/zoomPan";

export function ZoomPanCard() {
  return (
    <ZoomPanImage
      src="https://picsum.photos/id/1035/1600/1200"
      alt="A hiker looking over a canyon at dusk"
      className="zoomCard"
      zoom={{
        clickZoomLevel: 2.35,
        maxZoomLevel: 3.5,
      }}
    />
  );
}

ZoomPanImage is the lightweight standalone zoom surface. The component root is the clipping container, so border radius, aspect ratio, and overflow all live on the same element.

Skeleton

import { Skeleton, type SkeletonNode } from "react-motion-gallery/skeleton/base";

const shellSkeleton: SkeletonNode = {
  kind: "rect",
  style: { width: "100%", height: 320 },
};

export function LoadingShell({ ready, children }: { ready: boolean; children: React.ReactNode }) {
  return (
    <Skeleton
      layout={shellSkeleton}
      ready={ready}
      timing={{ exitMs: 520, minVisibleMs: 220 }}
      force={false}
      ariaLabel={ready ? undefined : "Loading content"}
    >
      {children}
    </Skeleton>
  );
}

Skeleton can render a standalone placeholder by itself, or it can wrap real content and own the loading transition. Wrapper mode is enabled when children are provided.

| Option | Type | Default | Notes | | --- | --- | --- | --- | | layout | SkeletonNode | | Structured placeholder layout tree. | | children | React.ReactNode | | Real content. When present, Skeleton renders content and loading layers. | | ready | boolean | false | Reveals content and exits the skeleton once true. | | enabled | boolean | true | Set false to render content immediately with no skeleton layer. | | force | boolean \| { enabled?: boolean; showContent?: boolean; skeletonOpacity?: number } | false | Keeps the skeleton visible. Set showContent: true to preview ready content under the skeleton, and tune the overlay with skeletonOpacity. | | timing.exitMs | number | 600 | Keeps the skeleton layer mounted for this long after exit starts and controls the opacity transition. | | timing.minVisibleMs | number | 220 | Minimum time the skeleton stays visible before exit can begin. | | shellClassName / shellStyle | string / CSSProperties | | Wrapper-layer class and style for content+skeleton mode. | | contentClassName / contentStyle | string / CSSProperties | | Content-layer class and style for wrapper mode. | | cache | SkeletonCacheOptions | | Opts into the cookie snapshot cache. Available on standalone Skeleton, SliderSkeleton, GridSkeleton, MasonrySkeleton, and Entries.loading.cache. |

The wrapper timing model matches the gallery loading layers: content begins fading in as soon as the skeleton exit starts; it does not wait for the skeleton to unmount.

Browser-measured skeleton text authoring

Responsive text is one of the easiest places for a polished loading state to drift away from the real UI. React Motion Gallery's skeleton text workflow measures real DOM text in a live page with headless Chrome, then emits lines, barWidth, lastBarWidth, and optional barHeight/lineHeight values for the skeleton text nodes used by Slider, Grid, Masonry, Entries, and standalone Skeleton layouts.

This is development-time authoring support, not production client code. It is especially useful for multiline cards, responsive grids, equal-height sliders, and reflow-sensitive masonry surfaces where a generic text placeholder can otherwise change row height, item height, or column packing when real content appears.

npm run --silent generate:skeleton-text-module -- \
  --input ./path/to/example.skeleton-text.browser.manifest.json \
  --analysis-output ./path/to/example.skeleton-text.measurements.json

Use responsiveBy: "container" when text wrapping follows the card or cell width more closely than the viewport. For equal-height card sliders, the browser analyzer can also measure all canonical slider items and emit rowHeightCompensation so unseen cards cannot surprise the skeleton row height. See docs/skeleton-text-authoring.md for manifest fields, command options, and the Codex-friendly workflow.

Skeleton cookie snapshot cache

The skeleton cookie snapshot cache is an opt-in SSR acceleration path for skeletons with expensive responsive text or layout geometry. The first visit uses the normal responsive skeleton CSS. After hydration, and again after debounced resizes, the client measures the active rendered content/skeleton geometry and writes a compact cookie. On a later server render, a valid cookie lets the skeleton render only the active snapshot values instead of the full responsive text CSS table.

This exists because very accurate skeletons can require a lot of responsive text CSS, especially when text wrapping affects masonry packing or card heights. Client-only storage such as sessionStorage cannot help SSR because the server cannot read it and the browser only gets it after the document starts executing. A cookie is available during SSR, so the server can reserve the correct first-paint geometry before hydration.

What the cache stores:

  • cache version, cache key, scope id, route key, timestamp, viewport width, and active width bucket
  • measured skeleton text records keyed by textId: line count, per-line widths, and optional bar metrics
  • masonry-only active geometry: variant key, shell height, and item heights
  • no text strings, no media URLs, and no full CSS text

Benefits:

  • first visit remains unchanged and uses the full responsive skeleton behavior
  • later reloads can parse much less skeleton CSS for the active breakpoint
  • text-heavy masonry, grid, slider, entries, and standalone skeletons can keep layout stability while reducing first-paint CSS work
  • stale, expired, route-mismatched, scope-mismatched, or malformed cookies silently fall back to the normal responsive path

Defaults: ttlMs is 10 * 60 * 1000, debounceMs is 250, cookie path is /, and sameSite is lax. Use a stable key per skeleton surface and a stable routeKey when a skeleton only applies to one route.

For skeleton text to be measurable, the skeleton text node needs a textId, and the matching real content text needs data-skeleton-text-id. Browser-generated skeleton text modules include textId automatically, so existing spreads such as ...skeletonText.body are cache-ready.

const cardBodySkeleton = {
  kind: "text",
  textId: "cardBody",
  barHeight: 14,
  lineHeight: 1.45,
  lines: { 0: 4, 900: 3, 1200: 2 },
} as const;

function CardBody({ children }: { children: React.ReactNode }) {
  return <p data-skeleton-text-id="cardBody">{children}</p>;
}

In SSR frameworks, read cache cookies on the server with the server-safe helper entry, then pass valid snapshots into a client provider. This example parses all React Motion Gallery skeleton cache cookies for the route.

// app/gallery/page.tsx
import { cookies } from "next/headers";
import {
  parseSkeletonCacheCookie,
  type SkeletonCacheSnapshot,
} from "react-motion-gallery/skeleton/cache";
import { GalleryPageClient } from "./GalleryPageClient";

function readSkeletonCacheSnapshots(
  cookieStore: Awaited<ReturnType<typeof cookies>>
) {
  const snapshots: Record<string, SkeletonCacheSnapshot> = {};

  for (const cookie of cookieStore.getAll()) {
    if (!cookie.name.startsWith("rmg_skel_cache_")) continue;

    const snapshot = parseSkeletonCacheCookie(cookie.value);
    if (snapshot) snapshots[snapshot.key] = snapshot;
  }

  return snapshots;
}

export default async function GalleryPage() {
  const snapshotMap = readSkeletonCacheSnapshots(await cookies());

  return <GalleryPageClient skeletonCacheSnapshots={snapshotMap} />;
}

Wrap the client tree in SkeletonCacheProvider, then opt individual skeletons in with cache={{ key, routeKey }}. Per-skeleton cache.snapshot takes precedence over provider snapshots when you need to pass one directly.

// app/gallery/GalleryPageClient.tsx
"use client";

import type { SkeletonCacheSnapshot } from "react-motion-gallery/skeleton/cache";
import { SkeletonCacheProvider } from "react-motion-gallery/skeleton/cache/provider";
import { MasonrySkeleton } from "react-motion-gallery/skeleton/masonry";
import { Masonry } from "react-motion-gallery/masonry";
import { useMasonryReady } from "react-motion-gallery/masonry/ready";

export function GalleryPageClient({
  skeletonCacheSnapshots,
}: {
  skeletonCacheSnapshots: Record<string, SkeletonCacheSnapshot | null | undefined>;
}) {
  const { ref, ready } = useMasonryReady();

  return (
    <SkeletonCacheProvider snapshots={skeletonCacheSnapshots}>
      <MasonrySkeleton
        cache={{
          key: "portfolio-masonry",
          routeKey: "/gallery",
        }}
        layout={portfolioSkeleton}
        ready={ready}
        masonry={{
          count: items.length,
          columns: { 0: 1, 720: 2, 1140: 4 },
          gap: { 0: 12, 1140: 18 },
        }}
      >
        <Masonry ref={ref} columns={{ 0: 1, 720: 2, 1140: 4 }}>
          {items.map((item) => (
            <Masonry.Item key={item.id}>{/* real card */}</Masonry.Item>
          ))}
        </Masonry>
      </MasonrySkeleton>
    </SkeletonCacheProvider>
  );
}

Use the same cache object on SliderSkeleton, GridSkeleton, and standalone Skeleton. For Entries, put it under entries.loading.cache.

<Entries
  entries={{
    items,
    mediaLayout: "grid",
    loading: {
      cache: {
        key: "editorial-entries",
        routeKey: "/stories",
      },
      skeleton: entrySkeleton,
    },
  }}
/>

Cookie options can be tuned per skeleton:

<GridSkeleton
  cache={{
    key: "product-grid",
    routeKey: "/products",
    ttlMs: 5 * 60 * 1000,
    debounceMs: 150,
    cookie: {
      path: "/",
      sameSite: "lax",
    },
  }}
  layout={productGridSkeleton}
/>

Slider

The default Slider is the small synchronous core: children, drag, wheel navigation, snapping, grouping, looping, index channels, intro, and the imperative ref API. Heavier behavior is opt-in through first-party plugins, so importing one feature, such as arrows or parallax, does not pull in the rest of the slider feature set. Structured slider skeletons and restore behavior are owned by SliderSkeleton, composed with useSliderReady().

import { Slider } from "react-motion-gallery/slider";
import { sliderArrows } from "react-motion-gallery/slider/arrows";

const slides = [
  "https://picsum.photos/id/1015/1600/900",
  "https://picsum.photos/id/1018/1600/900",
  "https://picsum.photos/id/1024/1600/900",
];

export function BasicSlider() {
  return (
    <Slider plugins={[sliderArrows()]}>
      {slides.map((src, index) => (
        <img key={src} src={src} alt={`Slide ${index + 1}`} style={{ width: "100%" }} />
      ))}
    </Slider>
  );
}

Slider component props

| Option | Type | Default | Notes | | --- | --- | --- | --- | | children | React.ReactNode | | Slide content rendered in order. | | initialIndex | number | 0 | Selects the slide index used for the first layout and intro fade-in. | | breakpoints | Record<string, number> | xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536 | Merged with the internal breakpoint map for responsive values. | | indexChannel | SliderIndexChannel | internal channel | Share index state with thumbnails or sibling sliders. | | plugins | SliderPlugin[] | [] | Explicit first-party slider features such as arrows, dots, auto-height, effects, fullscreen, or lazy-load. |

Slider layout and scroll options

| Option | Type | Default | Notes | | --- | --- | --- | --- | | layout.gap | number \| Record<string, number> | 20 | Responsive gap between cells. | | layout.cellsPerSlide | number \| Record<string, number> | | Groups multiple cells into a slide page. | | direction.dir | "ltr" \| "rtl" | "ltr" | Text direction and arrow direction. | | direction.axis | "x" \| "y" | "x" | Horizontal or vertical slider axis. | | align | "start" \| "center" | "start" | Slide alignment inside the viewport. | | scroll.groupCells | boolean | false | Scrolls by grouped cells instead of every cell. | | scroll.skipSnaps | boolean \| { enabled?: boolean; threshold?: number } | false | Allows momentum to skip snap points. Object form enables skip snaps by default and threshold requires release force to reach a multiple of the adjacent snap distance before multi-snap momentum is used. | | scroll.strictSnaps | boolean | false | Prevents one drag release from settling more than one snap away from where the drag started. Overrides scroll.skipSnaps. | | scroll.freeScroll | boolean | false | Enables free dragging instead of strict snapping. | | scroll.loop | boolean | false | Wraps around at the ends. |

Slider element and plugin options

elements, motion, and transitions.intro stay in the core slider. Controls, autoplay, lazy media, effects, auto-height, fullscreen, and loading overlays are explicit plugin imports.

| Option | Type | Default | Notes | | --- | --- | --- | --- | | elements.viewport | ElementStyle | | Class and inline style for the viewport element. | | elements.container | ElementStyle | | Class and inline style for the moving slider container. | | transitions.intro.renderIntro | ({ active, containerProps }, content) => ReactNode | | Custom intro wrapper. | | transitions.intro.staggerMs | number | | Delay between item fade-ins. | | transitions.intro.durationMs | number | | Intro fade duration. | | transitions.intro.easing | string | | Intro fade easing. |

Slider plugins

Each plugin is imported from its own subpath and passed to plugins. There is no aggregate controls or effects helper; this keeps one-feature imports as small as possible.

import { Slider } from "react-motion-gallery/slider";
import { sliderArrows } from "react-motion-gallery/slider/arrows";
import { sliderParallax } from "react-motion-gallery/slider/parallax";

<Slider plugins={[sliderArrows(), sliderParallax({ bleedPct: "8%" })]}>
  {slides}
</Slider>;

| Import | Factory | Notes | | --- | --- | --- | | react-motion-gallery/slider/arrows | sliderArrows(options) | Previous/next arrows. | | react-motion-gallery/slider/dots | sliderDots(options) | Pagination dots. | | react-motion-gallery/slider/progress | sliderProgress(options) | Progress bar or custom progress renderer. | | react-motion-gallery/slider/scrollbar | sliderScrollbar(options) | Range-style position control. | | react-motion-gallery/slider/ripple | sliderRipple(options) | Enables ripple feedback for controls that call createRipple. | | react-motion-gallery/slider/auto-play | sliderAutoPlay(options) | Timed slide changes. | | react-motion-gallery/slider/auto-scroll | sliderAutoScroll(options) | Timed continuous advancement. | | react-motion-gallery/slider/auto-height | sliderAutoHeight(options) | Measures active slide height and gates slider readiness until measured. | | react-motion-gallery/slider/lazy-load | sliderLazyLoad(options) | Adds lazy media attributes to slide images and videos. | | react-motion-gallery/slider/parallax | sliderParallax(options) | Parallax slide wrapper. | | react-motion-gallery/slider/scale | sliderScale(options) | Scales non-active slides. | | react-motion-gallery/slider/fade | sliderFade(options) | Fades non-active slides. | | react-motion-gallery/slider/crossfade | sliderCrossfade(options) | Enables crossfade-aware control navigation. | | react-motion-gallery/slider/fullscreen | sliderFullscreen() | Bridges a GalleryCore layout="slider" slider to fullscreen. | | react-motion-gallery/slider/loading | sliderLoading(options) | Basic custom loading overlay. Prefer SliderSkeleton for structured skeleton and restore. |

Slider loading skeletons

Use SliderSkeleton to own slider loading. useSliderReady() exposes the slider ref plus a settled ready flag; isSlidesBuilt() remains a lower-level DOM-built signal and is not the right fade-out trigger.

layout.slots is the per-slide override system. Define the shared placeholder once with layout.item and layout.itemWrapStyle, then override any individual slot with slots[index]. Slot itemWrapStyle values merge on top of the base wrap style, while slot.item can replace the placeholder node entirely for that slot.

itemWrapStyle now supports wrapper-only border and boxShadow values. Wrapper width, height, and aspectRatio are treated as outer border-box dimensions, so the inner placeholder shrinks by the border thickness. Use simple uniform border shorthands such as 1px solid #cbd5e1 when you want the built-in sizing math to account for the border width.

text nodes render one skeleton bar per lines value. barHeight controls the bar height and can be a single number or a numeric min-width map. lineHeight remains the full line-box multiplier and now accepts the same numeric min-width maps. lines can be a single number or a numeric min-width map such as { 0: 3, 767: 2, 1200: 1 }. Use lastBarWidth to override the shortened trailing bar width; it defaults to 68% of the text block width and can also be responsive with numeric min-width keys.

centering: "first" is designed for center-aligned peek sliders. When the real slider uses align="center" and the skeleton uses mode: "peek" with layout.kind: "slider", the skeleton renderer inserts the leading spacer needed to center the first visible placeholder. You should not add that spacer manually.

When you provide SliderSkeleton.timing, exitMs controls both how long the loading layer remains mounted after exit starts and its opacity transition duration.

import { SliderSkeleton } from "react-motion-gallery/skeleton/slider";
import { Slider } from "react-motion-gallery/slider";
import { useSliderReady } from "react-motion-gallery/slider/ready";

const slides = [
  { src: "https://picsum.photos/id/1020/660/960", width: 220, height: 320 },
  { src: "https://picsum.photos/id/1029/1020/630", width: 340, height: 320 },
  { src: "https://picsum.photos/id/1039/780/840", width: 260, height: 320 },
];

export function VariableWidthSkeletonSlider() {
  const { ref: sliderRef, ready: sliderReady } = useSliderReady();

  return (
    <SliderSkeleton
      ready={sliderReady}
      layout={{
        mode: "peek",
        centering: "first",
        visibleCount: 2,
        layout: {
          kind: "slider",
          direction: "row",
          style: { gap: 20 },
          item: {
            kind: "rect",
            style: {
              width: "100%",
              height: "100%",
              borderRadius: 12,
            },
          },
          slots: slides.map((slide) => ({
            itemWrapStyle: {
              width: slide.width,
              height: slide.height,
            },
          })),
        },
      }}
    >
      <Slider ref={sliderRef} align="center">
        {slides.map((slide, index) => (
          <img
            key={slide.src}
            src={slide.src}
            alt={`Slide ${index + 1}`}
            style={{ width: slide.width, height: slide.height, objectFit: "cover" }}
          />
        ))}
      </Slider>
    </SliderSkeleton>
  );
}

SliderSkeletonSpec

| Field | Type | Notes | | --- | --- | --- | | mode | "fit" \| "peek" | "peek" preserves partial next or previous slide visibility in the loading state. | | centering | "first" | Adds the leading spacer needed for the first visible slot when using the built-in centered peek skeleton flow. | | visibleCount | number \| Record<string, number> | Responsive count of visible skeleton slots. | | className | string \| undefined | Applied to the skeleton overlay root. | | style | React.CSSProperties \| undefined | Inline styles for the skeleton overlay root. | | layout | SliderSkeletonNode \| undefined | Structured placeholder layout tree. Use kind: "slider" to model slide tracks. | | backgroundColor | string \| undefined | Overrides the shared skeleton background color token. | | radius | number \| string \| undefined | Overrides the shared skeleton radius token. | | shimmer | SkeletonShimmer \| undefined | Shared shimmer settings for the entire skeleton tree. |

SliderSkeletonSliderNode

| Field | Type | Notes | | --- | --- | --- | | kind | "slider" | Slider-specific skeleton layout root. | | style | SkeletonContainerStyle \| Record<string, SkeletonContainerStyle> | Track-level container styles such as gap, padding, align, justify, width, and maxWidth. | | count | number \| undefined | Optional explicit slot count for the layout. Falls back to visibleCount on the surrounding slider skeleton spec. | | item | SkeletonNode | Default placeholder node rendered in each slot. | | itemWrapStyle | SliderSkeletonWrapStyle \| undefined | Shared wrapper size, margin, border, and box-shadow rules for every slot. Border sizing is border-box. | | slots | SliderSkeletonSlot[] \| undefined | Per-slot overrides for variable widths, heights, aspect ratios, or custom placeholder nodes. | | direction | "row" \| "col" \| undefined | Slot flow direction. centering: "first" only affects row layouts. | | children | SkeletonNode[] \| undefined | Optional extra skeleton content rendered after the slider row. It does not affect --rmg-slider-initial-height or reserve live layout space. |

SliderSkeletonSlot

| Field | Type | Notes | | --- | --- | --- | | item | SkeletonNode \| undefined | Replaces the base layout.item for one slot. | | itemWrapStyle | SliderSkeletonWrapStyle \| undefined | Merges on top of the base layout.itemWrapStyle for one slot, including wrapper borders and shadows. |

SkeletonNode supports these building blocks: rect, square, circle, text, media, row, col, and stack. text.barHeight controls the bar height, text.lines controls how many wrapped skeleton rows render for that text block, and text.lastBarWidth controls the trailing bar width.

Slider motion options

| Option | Type | Default | Notes | | --- | --- | --- | --- | | motion.selectDuration | number | 25 | Duration for snapped selection motion. | | motion.freeScrollDuration | number | 43 | Duration for free-scroll settling. | | motion.friction | number | 0.68 | Drag and settling friction. |

Slider render callback args

ArrowRenderArgs

| Field | Type | Notes | | --- | --- | --- | | ref | React.RefObject<HTMLDivElement \| null> | Attach to the arrow root. | | onClick | () => void | Calls the built-in previous or next action. | | hidden | boolean | true when the arrow should not render visually. | | disabled | boolean | true when navigation is unavailable. | | createRipple | (el: HTMLElement) => void | Triggers the built-in ripple effect manually. | | className | string \| undefined | Resolved class name for the arrow root. |

DotsRenderArgs

| Field | Type | Notes | | --- | --- | --- | | ref | React.RefObject<HTMLDivElement \| null> | Attach to the dots root. | | count | number | Dot count. | | activeIndex | number | Current selected slide index. | | hidden | boolean | true when dots should be hidden. | | goTo | (index: number) => void | Navigate to a slide. | | getDotRef | (index: number) => (el: HTMLDivElement \| null) => void | Ref factory for each dot. | | createRipple | (el: HTMLElement) => void | Manual ripple trigger. | | classNameContainer | string \| undefined | Resolved root class name. | | classNameDot | string \| undefined | Resolved dot class name. |

ProgressRenderArgs

| Field | Type | Notes | | --- | --- | --- | | ref | React.Ref<HTMLDivElement> | Attach to the progress root. | | innerRef | React.Ref<HTMLDivElement> \| undefined | Attach to the fill element. | | hidden | boolean | true when the progress bar should be hidden. | | progress | number | Progress value from 0 to 1. | | axis | "x" \| "y" | Fill direction. | | className | string \| undefined | Root class name. | | style | React.CSSProperties \| undefined | Root inline style. | | innerClassName | string \| undefined | Fill class name. | | innerStyle | React.CSSProperties \| undefined | Fill inline style. |

SliderHandle methods

| Method | Signature | Notes | | --- | --- | --- | | centerSlider | () => void | Re-centers the slider after layout changes. | | getIndex | () => number | Current active slide index. | | setIndex | (i: number, mode?: IndexMode) => void | Jumps or animates to a slide. | | subscribeIndex | (fn: () => void) => () => void | Subscribes to index changes. | | slideIndexForCell | (cellIndex: number) => number | Maps a cell index to its slide index when using grouped cells. | | getRootNode | () => HTMLElement \| null | Outer slider root. | | getContainerNode | () => HTMLElement \| null | Moving slide container. | | getSlideNodes | () => HTMLElement[] | Current slide elements. | | getViewportNode | () => HTMLDivElement \| null | Scroll viewport. | | onSlidesBuilt | (cb: (nodes: HTMLElement[]) => void) => () => void | Runs when slide nodes are ready. | | whenSlidesBuilt | () => Promise<HTMLElement[]> | Promise form of onSlidesBuilt. | | isSlidesBuilt | () => boolean | true once the slide list is ready. | | onReady | (cb: (nodes: HTMLElement[]) => void) => () => void | Runs when the slider has built, measured, committed its index, and all plugin ready gates have cleared. | | whenReady | () => Promise<HTMLElement[]> | Promise form of onReady. | | isReady | () => boolean | true once the settled slider ready signal has fired. | | scrollNext | (mode?: IndexMode) => void | Advances one step. | | scrollPrev | (mode?: IndexMode) => void | Moves backward one step. | | canScrollNext | () => boolean | Whether next navigation is available. | | canScrollPrev | () => boolean | Whether previous navigation is available. | | scrollProgress | () => number | Current progress from 0 to 1. | | cellsInView | () => number[] | Canonical cell indexes currently visible. | | append | (nodes: ReactNode \| ReactNode[]) => number | Appends nodes and returns the new total count. | | prepend | (nodes: ReactNode \| ReactNode[]) => number | Prepends nodes and returns the new total count. | | insert | (index: number, nodes: ReactNode \| ReactNode[]) => number | Inserts nodes and returns the new total count. | | remove | (indexOrPredicate: number \| ((i: number) => boolean)) => number | Removes items and returns the new total count. | | replace | (index: number, node: ReactNode) => void | Replaces a node at an index. | | setItems | (nodes: ReactNode[]) => number | Replaces all nodes and returns the new total count. | | onIndexChange | (cb: (i: number, meta: { mode: IndexMode }) => void) => () => void | Subscribes to index changes. | | getInternals | () => { slides, slider, visibleImages, selectedIndex, sliderX, sliderVelocity, isWrapping } | Low-level internals used by fullscreen and advanced sync code. |

createSliderIndexChannel

import { Slider, createSliderIndexChannel } from "react-motion-gallery";

const channel = createSliderIndexChannel();

export function SharedIndexSlider() {
  return (
    <Slider indexChannel={channel}>
      <div>One</div>
      <div>Two</div>
      <div>Three</div>
    </Slider>
  );
}

| Method | Signature | Notes | | --- | --- | --- | | createSliderIndexChannel | (initialIndex = 0, initialMode = "animated") => SliderIndexChannel | Creates a shared index event bus. | | get | () => { index: number; mode: IndexMode } | Reads the stored index and mode. | | set | (next: number, mode?: IndexMode, opts?: { silent?: boolean }) => void | Sets the current index and emits a "set" event unless silenced. | | bump | (delta: number, mode?: IndexMode, opts?: { silent?: boolean }) => void | Emits a relative index change event. | | subscribe | (fn: () => void) => () => void | Subscribes to channel updates. | | onEvent | (fn: (ev: IndexEvent) => void) => () => void | Receives the last "set" or "bump" event payload. | | onBasePointerDown | (fn: () => void) => () => void | Subscribes to base slider pointer-down events. | | emitBasePointerDown | () => void | Broadcasts a pointer-down event to subscribers. |

ThumbnailSlider

Use ThumbnailSlider when you want a synced thumbnail rail for a base Slider. In the common case, share one createSliderIndexChannel() instance and pass it to both components.

import {
  Slider,
  ThumbnailSlider,
  createSliderIndexChannel,
} from "react-motion-gallery";

const slides = [
  "https://picsum.photos/id/1015/1600/900",
  "https://picsum.photos/id/1018/1600/900",
  "https://picsum.photos/id/1024/1600/900",
];

const channel = createSliderIndexChannel();

export function SliderWithThumbnails() {
  return (
    <>
      <Slider indexChannel={channel}>
        {slides.map((src, index) => (
          <img key={src} src={src} alt={`Slide ${index + 1}`} style={{ width: "100%" }} />
        ))}
      </Slider>
      <ThumbnailSlider
        indexChannel={channel}
        options={{
          layout: { position: "bottom", gap: 8, thumbnail: { width: 88, height: 56 } },
          scroll: { centerActiveThumb: true },
          controls: { enabled: true },
        }}
      >
        {slides.map((src, index) => (
          <img
            key={`thumb-${src}`}
            src={src}
            alt={`Thumbnail ${index + 1}`}
            style={{ width: "100%", height: "100%", objectFit: "cover" }}
          />
        ))}
      </ThumbnailSlider>
    </>
  );
}

The component forwards a ref to its outer thumbnail shell.

ThumbnailSlider component props

| Option | Type | Default | Notes | | --- | --- | --- | --- | | children | React.ReactNode | | Thumbnail nodes rendered in order. Overrides options.children when both are provided. | | options | ThumbnailsOptions | | Base thumbnail configuration object. | | indexChannel | SliderIndexChannel | internal channel | Share the same channel as a base Slider to keep selection in sync. | | breakpoints | Record<string, number> | xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536 | Used to resolve layout.position and responsive loading counts. | | onThumbnailClick | (index: number) => void | | Fired when a thumbnail click publishes a selection to the shared channel. | | onReadyChange | (ready: boolean) => void | | Fired when the thumbnail rail finishes or re-enters its loading/layout cycle. | | direction | "ltr" \| "rtl" | "ltr" | Affects horizontal arrow direction and RTL scroll behavior. |

Thumbnail layout and scroll options

| Option | Type | Default | Notes | | --- | --- | --- | --- | | children | React.ReactNode | | Fallback thumbnail content when component children are omitted. | | layout.position | ResponsivePosition | "bottom" | Thumbnail rail position: "top", "right", "bottom", or "left". | | layout.gap | number | 8 | Gap between thumbnails. | | layout.center | boolean | false | Centers the overall rail content within its container when possible. | | layout.thumbnail.width | number \| string | | Width for each thumbnail item. | | layout.thumbnail.height | number \| string | | Height for each thumbnail item. | | layout.container.width | number \| string | | Width for the outer thumbnail container. | | layout.container.height | number \| string | | Height for the outer thumbnail container. | | scroll.freeScroll | boolean | true | Enables drag or wheel movement without strict snapping. | | scroll.groupCells | boolean | false | Pages the rail by grouped thumbnail cells. | | scroll.loop | boolean | false | Wraps thumbnails at the ends. | | scroll.skipSnaps | boolean | false | Allows momentum to skip snap points. | | scroll.centerActiveThumb | boolean | false | Repositions the rail to keep the active thumbnail centered. |

ResponsivePosition accepts a single side, an array, or a breakpoint map. For arrays, the first entry is used.

Thumbnail element, control, and motion options

| Option | Type | Default | Notes | | --- | --- | --- | --- | | elements.container | ElementStyle | | Class and inline style for the outer thumbnail container. | | elements.thumbnail | ElementStyle | | Class and inline style for each thumbnail item shell. | | controls.enabled | boolean | false | Shows previous and next arrows when the rail overflows. | | controls.arrow | ElementStyle | | Shared arrow class and style. | | controls.prev | ElementStyle | | Previous-arrow override. | | controls.next | ElementStyle | | Next-arrow override. | | controls.render | (args: ArrowRenderArgs & { dir: "prev" \| "next" }) => ReactNode | | Custom renderer for both thumbnail arrows. | | controls.renderPrev | (args: ArrowRenderArgs) => ReactNode | | Custom previous arrow. | | controls.renderNext | (args: ArrowRenderArgs) => ReactNode | | Custom next arrow. | | controls.ripple.enabled | boolean | true | Enables ripple feedback for thumbnail arrows. | | controls.ripple.className | string | | Custom ripple class for the arrow feedback element. | | motion.selectDuration | number | 25 | Duration for snapped thumbnail selection motion. | | motion.freeScrollDuration | number | 43 | Duration for free-scroll settling. | | motion.friction | number | 0.68 | Drag and settling friction. | | breakpointMap | Record<string, number> | xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536 | Override map used for responsive thumbnail positions and loading counts. |

Thumbnail transition options

| Option | Type | Default | Notes | | --- | --- | --- | --- | | transitions.loading.enabled | boolean | true | Enables the thumbnail loading layer. | | transitions.loading.force | boolean \| { enabled?: boolean; showContent?: boolean; skeletonOpacity?: number } | false | Forces the loading layer to remain visible. Set showContent: true to preview the real thumbnails under the skeleton, and tune the loading overlay with skeletonOpacity. | | transitions.loading.skeletonCount | number \| Record<string, number> | | Responsive count for the built-in loading placeholders. | | transitions.loading.mode | "fit" \| "peek" | "peek" | "peek" keeps fixed-size thumbnail placeholders when width or height is explicitly set; "fit" divides the rail evenly across the visible count. | | transitions.loading.elements.container | ElementStyle | | Class and inline style for the built-in loading overlay container. | | transitions.loading.elements.row | ElementStyle | | Class and inline style for the built-in skeleton row or column wrapper. | | transitions.loading.elements.thumbnail | ElementStyle | | Class and inline style for each built-in thumbnail placeholder. | | transitions.loading.renderLoading | ({ count }) => ReactNode | | Replaces the built-in thumbnail loading skeleton and receives the resolved responsive count. | | transitions.loading.timing.exitMs | number | 600 | Keeps the thumbnail loading layer mounted for this long after exit starts. | | transitions.loading.timing.minVisibleMs | number | 220 | Minimum time the loading layer stays visible before exit can begin. | | transitions.intro.renderIntro | ({ active, containerProps }, inner) => ReactNode | | Custom intro wrapper for the thumbnail rail. | | transitions.intro.staggerMs | number | 40 | Delay between thumbnail fade-ins. | | transitions.intro.durationMs | number | 300 | Intro fade duration. | | transitions.intro.easing | string | "cubic-bezier(.2,.7,.2,1)" | Intro fade easing. |

transitions.loading.elements.* only applies to the built-in thumbnail skeleton. If you provide transitions.loading.renderLoading, you fully own the loading markup instead.

The built-in thumbnail placeholders use the same shimmer variable family as slider skeletons: --rmg-skel-bg, --rmg-skel-shimmer-enabled, --rmg-skel-shimmer-opacity, --rmg-skel-shimmer-filter, --rmg-skel-shimmer-angle, --rmg-skel-shimmer-c1, --rmg-skel-shimmer-c2, --rmg-skel-shimmer-c3, --rmg-skel-shimmer-duration, and --rmg-skel-shimmer-timing.

For thumbnails, transitions.loading.timing.exitMs controls both the mounted exit lifetime and the loading-layer opacity fade. The thumbnail intro can begin as soon as the loading exit starts.

createThumbnailSyncBridge

ThumbnailSlider creates and starts this bridge for you internally when you pass indexChannel. Reach for createThumbnailSyncBridge() only when you need to wire a local thumbnail rail to an external slider channel manually.

| Method | Signature | Notes | | --- | --- | --- | | createThumbnailSyncBridge | (args: { localChannel, externalChannel?, clampIndex? }) => ThumbnailSyncBridge | Creates a bridge between local thumbnail state and an optional external slider channel. | | start | () => () => void | Starts syncing and returns a cleanup function. | | stop | () => void | Stops syncing without disposing the channels. | | publishThumbnailClick | (index: number, mode?: IndexMode) => void | Publishes a thumbnail click to the external slider channel. |

Grid

import { Grid } from "react-motion-gallery";

const images = Array.from({ length: 6 }, (_, index) => ({
  src: `https://picsum.photos/seed/grid-${index}/1200/1200`,
  alt: `Grid item ${index + 1}`,
}));

export function BasicGrid() {
  return (
    <Grid columns={{ 0: 1, 640: 2, 960: 3 }} gap={{ 0: 12, 960: 20 }}>
      {images.map((image) => (
        <img key={image.src} src={image.src} alt={image.alt} style={{ width: "100%" }} />
      ))}
    </Grid>
  );
}

Grid component props

| Option | Type | Default | Notes | | --- | --- | --- | --- | | children | React.ReactNode | | Grid items rendered in order. Wrap individual cards in Grid.Item when they need custom spans or wrapper props. | | breakpoints | Record<string, number> | xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536 | Used to resolve responsive columns and gaps. | | gridItemBaseClass | string | "rmg__grid-item" | Internal item base class override. | | renderMode | "wrap" \| "passthrough" | "wrap" | wrap adds an item wrapper; passthrough keeps child structure closer to the source node. |

Grid.Item props

Grid.Item is a metadata wrapper. It renders only its children, while Grid reads the wrapper props and applies them to the generated item shell.

| Option | Type | Default | Notes | | --- | --- | --- | --- | | children | React.ReactNode | | The grid card content. | | span | number \| "full" \| Record<string, number \| "full"> | 1 | Per-item track span. "full" renders grid-column: 1 / -1; numeric values render grid-column: span n / span n. | | className | string | | Extra class name merged onto the grid item wrapper. | | style | React.CSSProperties | | Inline styles merged onto the grid item wrapper. |

Grid options

| Option | Type | Default | Notes | | --- | --- | --- | --- | | columns | number \| Record<string, number> | | Fixed responsive column count. When omitted, Grid auto-fits using minColumnWidth. | | templateColumns | string \| Record<string, string> | | Explicit grid-template-columns value. Takes precedence over columns and minColumnWidth. | | minColumnWidth | number \| string | 160 | Minimum width used by auto-fit mode. | | gap | number \| Record<string, number> | 8 | Responsive grid gap. | | rootClassName | string | | Class name for the grid root. | | itemClassName | string | | Class name added to each wrapped grid item. | | fullscreenTrigger | "item" \| "media" | "media" | Opens fullscreen from the clicked media node or the entire item shell. | | plugins | GridPlugin[] | [] | Explicit first-party Grid features such as lazy-load. | | intro.renderIntro | ({ active, containerProps }, content) => ReactNode | | Custom intro wrapper. | | intro.staggerMs | number | 60 | Reveal stagger for the fade-in. | | intro.durationMs | number | 600 | Intro fade duration. | | intro.easing | string | "cubic-bezier(.2,.7,.2,1)" | Intro fade easing. | | intro.staggerLimit | number | | Optional cap on how many items stagger. |

Grid plugins

Import Grid plugins from their own subpaths and pass them to plugins.

import { Grid } from "react-motion-gallery/grid";
import { gridLazyLoad } from "react-motion-gallery/grid/lazy-load";

<Grid plugins={[gridLazyLoad({ spinner: true })]}>{items}</Grid>;

| Import | Factory | Notes | | --- | --- | --- | | react-motion-gallery/grid/lazy-load | gridLazyLoad(options) | Rewrites trackable image src values into data-rmg-lazy-src, reveals them on viewport intersection, then fades them in after decode and spinner exit. |

gridLazyLoad() enables lazy loading by default. Pass { enabled: false } to make the plugin inert.

Grid fullscreen behavior is provided by GalleryCore and useFullscreenController; Grid itself does not expose a ref-based imperative API.

Wrap a card in Grid.Item when it should span tracks or needs wrapper styling:

<Grid columns={{ 0: 1, 720: 6, 1100: 12 }} gap={{ 0: 12, 1100: 18 }}>
  <Grid.Item span={{ 0: "full", 720: 3, 1100: 6 }} className="feature-card">
    <FeatureCard />
  </Grid.Item>
  <Grid.Item span={{ 0: "full", 720: 3, 1100: 3 }}>
    <ProductCard />
  </Grid.Item>
  <Grid.Item span="full">
    <WideEditorialCard />
  </Grid.Item>
</Grid>

Grid spans require explicit tracks: use columns or templateColumns. If Grid is in auto-fit mode through minColumnWidth, item spans are ignored because there is no stable track count to span. Responsive span maps use the same breakpoint keys as responsive numeric props, so named keys such as md and numeric keys such as 900 are both valid.

Use templateColumns when the tracks themselves need custom proportions:

<Grid
  templateColumns={{
    0: "1fr",
    900: "minmax(0, 1.4fr) minmax(0, 1fr)",
    1200: "minmax(0, 2fr) repeat(2, minmax(0, 1fr))",
  }}
  gap={{ 0: 12, 1200: 18 }}
>
  <Grid.Item span={{ 0: "full", 900: 2 }}>
    <FeatureCard />
  </Grid.Item>
</Grid>

Grid no longer owns loading UI. Use useGridReady and wrap Grid with GridSkeleton, the same composition pattern used by Slider and Masonry.

Grid skeletons live in react-motion-gallery/skeleton/grid. Their text nodes use the same wrapped-line treatment as slider skeletons, including responsive barHeight and lines maps plus the configurable trailing lastBarWidth.

Grid skeletons inherit real item spans by default. Slot overrides in the Skeleton layout can change individual placeholder nodes or wrapper styles without losing the span applied by Grid.Item.

When Grid is wrapped in GridSkeleton, GridSkeleton.timing.exitMs controls both how long the loading layer stays mounted after exit starts and its opacity transition, and the real grid intro begins as soon as exit starts.

import { Grid, useGridReady } from "react-motion-gallery";
import { GridSkeleton, type GridSkeletonSpec } from "react-motion-gallery/skeleton/grid";

const gridSkeleton: GridSkeletonSpec = {
  radius: 14,
  layout: {
    kind: "grid",
    count: 6,
    item: {
      kind: "rect",
      style: { aspectRatio: "4 / 5" },
    },
  },
};

function GridWithSkeleton({ images }: { images: { src: string; alt: string }[] }) {
  const { ref: gridRef, ready: gridReady } = useGridReady();

  return (
    <GridSkeleton
      layout={gridSkeleton}
      ready={gridReady}
      timing={{ minVisibleMs: 220, exitMs: 600 }}
      grid={{
        count: images.length,
        columns: { 0: 1, 640: 2, 960: 3 },
        gap: { 0: 12, 960: 20 },
      }}
    >
      <Grid
        ref={gridRef}
        columns={{ 0: 1, 640: 2, 960: 3 }}
        gap={{ 0: 12, 960: 20 }}
      >
        {images.map((image) => (
          <img key={image.src} src={image.src} alt={image.alt} />
        ))}
      </Grid>
    </GridSkeleton>
  );
}

Masonry

import { Masonry } from "react-motion-gallery";

const cards = [280, 360, 220, 420, 300, 340];

export function BasicMasonry() {
  return (
    <Masonry columns={{ 0: 1, 700: 2, 1100: 3 }} gap={{ 0: 12, 1100: 20 }}>
      {cards.map((height, index) => (
        <img
          key={index}
          src={`https://picsum.photos/seed/masonry-${index}/1000/${height * 3}`}
          alt={`Masonry item ${index + 1}`}
          style={{ width: "100%", height, objectFit: "cover", borderRadius: 12 }}
        />
      ))}
    </Masonry>
  );
}

Masonry component props

| Option | Type | Default | Notes | | --- | --- | --- | --- | | children | React.ReactNode | | Masonry items rendered in order. Wrap individual cards in Masonry.Item when they need custom spans or wrapper props. | | breakpoints | Record<string, number> | xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536 | Used to resolve responsive columns and gaps. |

Masonry.Item props

| Option | Type | Default | Notes | | --- | --- | --- | --- | | children | React.ReactNode | | The masonry card content. | | span | number \| "full" \| Record<string, number \| "full"> | 1 | Per-item track span. "full" resolves to the active column count and numeric values clamp to the current track count. | | className | string | | Extra class name merged onto the masonry item wrapper. | | style | React.CSSProperties | | Inline styles merged onto the masonry item wrapper. |

Masonry options

| Option | Type | Default | Notes | | --- | --- | --- | --- | | columns | number \| Record<string, number> | | Responsive column count. | | gap | number \| Record<string, number> | | Responsive gap between columns and items. | | placement | "balanced" \| "roundRobin" \| "horizontalOrder" | "balanced" | balanced packs into the shortest fitting column group, roundRobin cycles start columns deterministically, and horizontalOrder preserves a stronger left-to-right scan when spans are involved. | | fullscreenTrigger | "item" \| "media" | "media" | Opens fullscreen from the clicked media node or the entire masonry item shell. | | itemWrapClassName | string | | Class name added to the masonry item wrapper. | | itemWrapStyle | React.CSSProperties | | Inline styles applied to the masonry item wrapper. | | as | React.ElementType | "div" | Root HTML element or custom component. | | rootRef | React.Ref<HTMLDivElement> | | Ref to the masonry root. | | classNames.root | string | | Root class name. | | classNames.column | string | | Retained for backwards compatibility with the legacy column-wrapper renderer. | | classNames.item | string | | Item class name. | | plugins | MasonryPlugin[] | [] | Explicit first-party Masonry features such as lazy-load. | | intro.renderIntro | ({ active, containerProps }, content) => ReactNode | | Custom intro wrapper. | | intro.staggerMs | number | 160 | Reveal stagger for the fade-in. | | intro.durationMs | number | 600 | Intro fade duration. | | intro.easing | string | "cubic-bezier(.2,.7,.2,1)" | Intr