react-motion-gallery
v2.0.99
Published
React gallery primitives for sliders, carousels, grid, masonry, fullscreen lightbox, entries, video, zoom/pan/pinch, skeleton loading and reveal animations
Maintainers
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 | 16.1kB |
| entries/media/slider | 24.6kB |
| entries/media/grid | 21.0kB |
| entries/media/masonry | 18.9kB |
| entries/ready | 360.0B |
| entries/pagination | 242.0B |
| entries/load-more | 198.0B |
| entries/infinite-scroll | 208.0B |
| entries/virtualization | 236.0B |
| rating-stars | 1.3kB |
| FullscreenThumbnailSlider | 26.3kB |
| GalleryCore | 2.7kB |
| Grid | 18.0kB |
| grid/ready | 323.0B |
| grid/lazy-load | 3.7kB |
| grid/fullscreen | 1.6kB |
| grid/pagination | 246.0B |
| grid/load-more | 236.0B |
| grid/infinite-scroll | 1.7kB |
| grid/virtualization | 256.0B |
| Masonry | 12.2kB |
| masonry/ready | 323.0B |
| masonry/fullscreen | 1.1kB |
| masonry/lazy-load | 3.7kB |
| masonry/text-wrap | 1.8kB |
| masonry/pagination | 239.0B |
| masonry/load-more | 227.0B |
| masonry/infinite-scroll | 1.7kB |
| masonry/virtualization | 252.0B |
| Skeleton base | 9.1kB |
| skeleton/slider | 14.7kB |
| skeleton/slider/restore | 25.3kB |
| skeleton/grid | 11.4kB |
| skeleton/masonry | 4.8kB |
| Slider core | 22.2kB |
| slider/ready | 983.0B |
| slider/arrows | 1.2kB |
| slider/dots | 927.0B |
| slider/progress | 892.0B |
| slider/scrollbar | 1.7kB |
| slider/auto-height | 1.3kB |
| slider/lazy-load | 4.2kB |
| slider/parallax | 1.4kB |
| slider/scale | 1.2kB |
| slider/fade | 1.2kB |
| slider/crossfade | 3.0kB |
| slider/fullscreen | 1.5kB |
| ThumbnailSlider | 24.6kB |
| useFullscreenController | 6.2kB |
| fullscreen/slider | 50.3kB |
| fullscreen/controls | 173.0B |
| fullscreen/captions | 14.4kB |
| fullscreen/zoom-pan | 12.8kB |
| fullscreen/video | 17.7kB |
| fullscreen/lazy-load | 14.4kB |
| fullscreen/crossfade | 181.0B |
| fullscreen/thumbnails | 160.0B |
| Video | 13.2kB |
| ZoomPanImage | 11.2kB |
| zoomPan/hover | 124.0B |
| media / toMediaItems | 260.0B |
| media/ready | 656.0B |
| responsive / BREAKPOINT_MAP | 85.0B |
| Reveal | 2.4kB |
Installation
Install the package:
npm install react-motion-galleryIf you use Video or fullscreen video playback, also install the optional Plyr peers:
npm install plyr plyr-reactImport 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";Most examples in this README use hooks, event handlers, or browser-only behavior. In Next.js App Router, put those components in a client file with "use client";; server components can still prepare media data and pass it down.
License
React Motion Gallery is licensed under PolyForm-Noncommercial-1.0.0. Non-commercial use is free. Commercial use requires a paid license; see react-motion-gallery.com/license.
Overview
Mental model:
Slider,Grid, andMasonryrender React children directly.Entriesrenders structured entry data with a custom media container.GalleryCoreanduseFullscreenControllerpower fullscreen behavior.Videois the gallery-ready video primitive.ZoomPanImageattaches click-to-zoom, drag pan, ctrl-wheel pinch, and touch pinch to one clipped image surface.Skeletonrenders 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://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=1600&h=900&q=80",
{ src: "https://images.unsplash.com/photo-1475924156734-496f6cac6ec1?auto=format&fit=crop&w=1600&h=900&q=80", 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 | Aggregate root for primary components, helpers, and companion public types |
| react-motion-gallery/styles.css | Compiled stylesheet required by gallery primitives and controls |
| react-motion-gallery/media | toMediaItems, MediaItem, MediaInput |
| react-motion-gallery/media/ready | useImageDecodeReady |
| react-motion-gallery/responsive | BREAKPOINT_MAP and responsive value types |
| react-motion-gallery/reveal | Reveal, useReveal, reveal types |
| react-motion-gallery/rating-stars | RatingStars |
| react-motion-gallery/core | GalleryCore, GalleryCoreProvider, useGalleryCore |
| react-motion-gallery/slider | Slider, createSliderIndexChannel, slider and fixed-track virtualization 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/grid/fullscreen | gridFullscreen for Grid + GalleryCore |
| react-motion-gallery/grid/pagination | gridPagination, useGridPagination, GridPaginationControls, page range helpers |
| react-motion-gallery/grid/load-more | gridLoadMore, useGridLoadMore |
| react-motion-gallery/grid/infinite-scroll | gridInfiniteScroll, useGridInfiniteScroll |
| react-motion-gallery/grid/virtualization | gridVirtualization, useGridVirtualizer |
| react-motion-gallery/masonry | Masonry, Masonry.Item, masonry types |
| react-motion-gallery/masonry/ready | useMasonryReady |
| react-motion-gallery/masonry/fullscreen | masonryFullscreen for light Masonry + GalleryCore |
| react-motion-gallery/masonry/lazy-load | masonryLazyLoad |
| react-motion-gallery/masonry/pagination | masonryPagination, useMasonryPagination, MasonryPaginationControls, page range helpers |
| react-motion-gallery/masonry/load-more | masonryLoadMore, useMasonryLoadMore |
| react-motion-gallery/masonry/infinite-scroll | masonryInfiniteScroll, useMasonryInfiniteScroll |
| react-motion-gallery/masonry/virtualization | masonryVirtualization, useMasonryVirtualizer |
| react-motion-gallery/entries | Entries, flattenEntries, entry data plugins, hooks, and types |
| react-motion-gallery/entries/media/slider | createEntriesSliderMedia |
| react-motion-gallery/entries/media/grid | createEntriesGridMedia |
| react-motion-gallery/entries/media/masonry | createEntriesMasonryMedia |
| react-motion-gallery/entries/ready | useEntriesReady |
| react-motion-gallery/entries/pagination | entriesPagination, useEntriesPagination, EntriesPaginationControls, page range helpers |
| react-motion-gallery/entries/load-more | entriesLoadMore, useEntriesLoadMore |
| react-motion-gallery/entries/infinite-scroll | entriesInfiniteScroll, useEntriesInfiniteScroll |
| react-motion-gallery/entries/virtualization | entriesVirtualization, useEntriesVirtualizer |
| 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 | Lightweight MasonrySkeleton for dimensioned placeholders |
| react-motion-gallery/skeleton/cache | Server-safe skeleton cookie cache helpers and types |
| react-motion-gallery/skeleton/cache/provider | Client SkeletonCacheProvider for SSR snapshots and client cookie refresh |
| react-motion-gallery/skeleton/slider/restore | SliderSkeleton with cache, plus RestoredSliderSkeleton for optional restore |
| react-motion-gallery/fullscreen | useFullscreenController, dialog handoff methods, 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, render-prop data, and virtualization types |
| react-motion-gallery/fullscreenThumbnails | FullscreenThumbnailSlider, including large fullscreen thumbnail rail virtualization |
| react-motion-gallery/video | Video and optional Plyr-backed video types |
| react-motion-gallery/zoomPan | ZoomPanImage and zoom/pan types |
| react-motion-gallery/zoomPan/hover | zoomPanHover |
For a named-export inventory that covers every public subpath, including type-only exports and lower-level helpers, see docs/public-api-inventory.md.
Data plugin imports are intentionally split by surface and behavior:
| Surface | Pagination | Load more | Infinite scroll | Virtualization |
| ------- | ----------------------------------------- | ---------------------------------------- | ---------------------------------------------- | --------------------------------------------- |
| Grid | react-motion-gallery/grid/pagination | react-motion-gallery/grid/load-more | react-motion-gallery/grid/infinite-scroll | react-motion-gallery/grid/virtualization |
| Masonry | react-motion-gallery/masonry/pagination | react-motion-gallery/masonry/load-more | react-motion-gallery/masonry/infinite-scroll | react-motion-gallery/masonry/virtualization |
| Entries | react-motion-gallery/entries/pagination | react-motion-gallery/entries/load-more | react-motion-gallery/entries/infinite-scroll | react-motion-gallery/entries/virtualization |
import { useGridPagination } from "react-motion-gallery/grid/pagination";
import { useMasonryLoadMore } from "react-motion-gallery/masonry/load-more";
import { entriesVirtualization } from "react-motion-gallery/entries/virtualization";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-mcpThen 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 toolsUser goal: "Build a product grid with image placeholders while loading."
Workflow: layoutWithNonTextSkeleton
Use: Skeleton rect/media nodes or gallery skeleton wrappers
Skip: browser text measurementUser goal: "Build a card layout with simple text skeleton lines."
Workflow: layoutWithHandAuthoredTextSkeleton
Use: text skeleton nodes with hand-authored lines/barWidth values
Skip: generated sidecarUser goal: "Build a masonry layout where skeleton text matches real responsive copy."
Workflow: layoutWithBrowserMeasuredTextSkeleton
Use: stable selectors -> probe_render_context -> scaffold_skeleton_text with renderReceiptId -> generate:skeleton-text-module --analysis-output -> import sidecarWhen 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-html5Call 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 as a dry run to get the exact probe_render_context call. Apply the browser-measurement manifest only after passing the returned receiptId as renderReceiptId.
{
"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,
"renderReceiptId": "rmg-render-...",
"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.
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. |
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. |
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.
Default skeleton imports are cache-free. The cache-backed public skeleton surface retained in this release is SliderSkeleton from react-motion-gallery/skeleton/slider/restore; use its restore option, or the RestoredSliderSkeleton alias, when slider reload/back-forward restore is needed. Non-slider gallery loading surfaces no longer accept loading.cache.
SkeletonFrame is also exported from react-motion-gallery/skeleton/base for lower-level composition when you already have a rendered skeleton node and want the shared wrapper timing/layering behavior.
| SkeletonFrame prop | Type | Default | Notes |
| ------------------------------ | ----------------------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------- |
| skeletonNode | React.ReactNode | required | Loading layer content. |
| children | React.ReactNode | — | Real content. Without children, SkeletonFrame returns skeletonNode directly. |
| ready | boolean | false | Reveals content when true. |
| enabled | boolean | true | Bypasses loading behavior when false. |
| force | boolean \| { enabled?: boolean; showContent?: boolean; skeletonOpacity?: number } | false | Keeps or compares the loading layer. |
| timing | { enterMs?, exitMs?, minVisibleMs? } | shared defaults | Loading-layer timing. |
| shellClassName / shellStyle | string / React.CSSProperties | — | Outer shell class and style. |
| loadingShellStyle | React.CSSProperties \| null | — | Style applied to the shell only while the loading layer is showing. |
| contentClassName / contentStyle | string / React.CSSProperties | — | Content layer class and style. |
| contentOwnsWrapperLayout | boolean | false | Keeps the wrapper sized by content when the content layer is not locked. |
| lockContentLayoutWhileLoading | boolean | false | Temporarily makes the loading layer the normal-flow owner and absolutely layers content over it. |
| loadingLayerFirst | boolean | false | Renders the loading layer before the content layer. |
| contentWrapper | (children: ReactNode) => ReactNode | — | Wraps content inside the shared skeleton reveal-gate provider. |
| shellDataAttributes | Record<string, string \| boolean \| undefined> | — | Extra data attributes for the shell. |
| loadingShellDataAttributes | Record<string, string \| boolean \| undefined> | — | Extra shell data attributes while loading is showing. |
| shellRef | React.Ref<HTMLDivElement> | — | Ref for the outer shell. |
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.jsonUse 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.
Slider skeleton cookie snapshot cache
The skeleton cookie snapshot cache remains available for slider skeletons. Use SliderSkeleton from react-motion-gallery/skeleton/slider/restore when a slider skeleton should read and write snapshot cookies, and add restore or use the RestoredSliderSkeleton alias when reload/back-forward restore is needed. Non-slider gallery loading surfaces no longer accept loading.cache, and the base/grid/masonry/entries cache wrapper subpaths have been removed.
In SSR frameworks, read cache cookies on the server with react-motion-gallery/skeleton/cache, pass snapshots through SkeletonCacheProvider, and opt slider skeletons in with a stable cache={{ key, routeKey }} object.
| Cache export | Notes |
| ------------ | ----- |
| getSkeletonCacheCookieName(key) | Returns the deterministic cookie name for a cache key. |
| getSkeletonCacheRouteKey(location?) | Builds the default route key from pathname + search, or "" without a location. |
| parseSkeletonCacheCookie(raw, options?) | Parses and validates a cookie value into SkeletonCacheSnapshot \| null. |
| serializeSkeletonCacheSnapshot(snapshot) | Serializes a snapshot into the compact cookie payload string. |
| validateSkeletonCacheSnapshot(snapshot, options?) | Re-validates an already parsed snapshot against key, route, kind, viewport, TTL, text ids, and item metadata. |
| SKELETON_CACHE_VERSION and default constants | Exported for diagnostics and custom integrations: TTL, debounce, per-cookie byte budget, and total cookie byte budget. |
| SkeletonCacheOptions field | Type | Notes |
| ---------------------------- | ---- | ----- |
| key | string | Required stable cache identity. |
| snapshot | SkeletonCacheSnapshot \| null | Server-provided snapshot override. |
| ttlMs | number | Snapshot freshness window. Defaults to DEFAULT_SKELETON_CACHE_TTL_MS. |
| debounceMs | number | Client write debounce. Defaults to DEFAULT_SKELETON_CACHE_DEBOUNCE_MS. |
| routeKey | string | Optional route guard for page-specific skeleton geometry. |
| cookie.path | string | Cookie path. Defaults to /. |
| cookie.sameSite | "lax" \| "strict" \| "none" | SameSite policy. Defaults to lax. |
| cookie.secure | boolean | Adds the Secure cookie attribute when true. |
| cookie.maxCookieBytes | number | Per-cookie write budget. Defaults to DEFAULT_SKELETON_CACHE_COOKIE_MAX_BYTES. |
| cookie.maxTotalCookieBytes | number | Combined React Motion Gallery cache-cookie budget. Defaults to DEFAULT_SKELETON_CACHE_COOKIE_MAX_TOTAL_BYTES. |
| Parse/validate option | Notes |
| --------------------- | ----- |
| key, scopeId, kind, routeKey | Reject snapshots that do not match the expected identity, cache kind, or route. |
| ttlMs, now | Override freshness validation and the timestamp used for age checks. |
| viewportWidth, viewportTolerancePx, widthBucketMin | Require compatible viewport or bucket metadata. |
| textIds | Require text measurements for specific skeleton text ids. |
| itemCount, variantKeys | Require compatible masonry snapshot metadata when validating older/custom snapshots. |
SkeletonCacheProvider accepts children, shared options, one snapshot, or a keyed snapshots map. Provider snapshots are used during hydration, then the client refreshes readable cache cookies after mounted slider skeletons measure.
Reveal
Reveal is a standalone entrance primitive for page sections and application UI. It is intentionally separate from skeleton loading and gallery reveal timing: content is already rendered, then opacity and optional transform animate when the element enters view.
Use <Reveal> by default. Use useReveal when you need to own the element, avoid an extra wrapper, merge refs, read revealed or inView, or build a higher-level abstraction. Reveal does not automatically wait for wrapped images to load or decode; pass ready when media readiness should gate the entrance.
import { useImageDecodeReady } from "react-motion-gallery/media/ready";
import { Reveal } from "react-motion-gallery/reveal";
export function ImageSectionReveal({ src, alt }: { src: string; alt: string }) {
const image = useImageDecodeReady({ src });
return (
<Reveal
as="figure"
ready={image.ready}
transform={{ y: 18, scale: 0.98 }}
durationMs={{ opacity: 220, transform: 680 }}
easing={{
opacity: "ease-out",
transform: "cubic-bezier(0.2, 0.7, 0.2, 1)",
}}
staggerIndex={1}
>
<img src={src} alt={alt} loading="eager" decoding="async" />
<figcaption>Fast fade, slower motion.</figcaption>
</Reveal>
);
}When you need to own the element directly, use useReveal with the same readiness gate:
import { useImageDecodeReady } from "react-motion-gallery/media/ready";
import { useReveal } from "react-motion-gallery/reveal";
export function DecodedImageReveal({ src, alt }: { src: string; alt: string }) {
const image = useImageDecodeReady({ src });
const reveal = useReveal<HTMLElement>({
ready: image.ready,
transform: { y: 18, scale: 0.98 },
});
return (
<figure
{...reveal.revealProps}
ref={reveal.ref}
className={reveal.revealProps.className}
style={reveal.revealProps.style}
>
<img src={src} alt={alt} loading="eager" decoding="async" />
</figure>
);
}Reveal props and options
Reveal is polymorphic through as; element-specific props are forwarded to the rendered element after the reveal options are removed. useReveal(options) accepts the same RevealOptions behavior props and returns props you can spread onto an element you own.
| Prop | Type | Default | Notes |
| ------------------- | ------------------------------------------------------------ | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| as | React.ElementType | "div" | Render as another element or component, such as "section", "figure", or a custom component. |
| children | React.ReactNode | — | Content rendered inside the revealed element. |
| className | string | — | Merged with the internal reveal class. |
| style | React.CSSProperties | — | Merged onto the rendered element. A string style.transform becomes the final resting transform for transform reveals. |
| ref | React.Ref<HTMLElement> | — | Forwarded to the rendered element. |
| variant | "fade" \| "transform" | "transform" | fade animates opacity only; transform also animates from the configured transform. |
| transform | RevealTransform | { y: 14 } | Typed transform object or raw CSS transform string used as the hidden/from transform. |
| once | boolean | true | Keeps the element revealed after the first reveal. Set false for reversible in-view reveals. |
| ready | boolean | true | Gates the reveal until external readiness, such as image decode, has completed. With once: false, dropping ready back to false can hide the element again. |
| threshold | number | 0.12 | Intersection ratio required before reveal. Values are clamped between 0 and 1. |
| rootMargin | string | "0px 0px -8% 0px" | IntersectionObserver root margin used to tune when the element enters view. |
| durationMs | RevealDuration | 520 | Scalar timing applies to opacity and transform. Object timing can set { opacity, transform } separately. |
| delayMs | number | 0 | Base delay before the reveal animation starts. Negative values are clamped to 0. |
| staggerIndex | number | 0 | Multiplies staggerMs and adds to delayMs; useful for repeated items. Negative values are clamped to 0. |
| staggerMs | number | 70 | Delay step used with staggerIndex. Negative values are clamped to 0. |
| easing | RevealEasing | "cubic-bezier(0.2, 0.7, 0.2, 1)" | Scalar easing applies to both channels. Object easing can set { opacity, transform } separately. |
| disabled | boolean | false | Bypasses observer gating and renders as revealed. |
| onReveal | () => void | — | Called when the element transitions from hidden to revealed. Not called for disabled reveals. |
| Other element props | Omit<React.ComponentPropsWithoutRef<E>, reveal-owned keys> | — | Forwarded to the rendered element, including ARIA attributes and event handlers, after reveal-owned props are removed. |
Reveal transform fields
Numbers in length fields become px; numbers in angle fields become deg. Pass a raw string as transform when you need complete CSS control.
| Field | Type | Default | Notes |
| ------------------------------ | -------------- | ------------------------------------- | ------------------------------------------------------------------------ |
| x, y, z | RevealLength | 0px when any translate field is set | Builds translate3d(x, y, z). |
| scale | number | 1 | Uniform scale used by both axes unless scaleX or scaleY is provided. |
| scaleX, scaleY | number | scale ?? 1 | Axis-specific scale values. |
| rotate, rotateX, rotateY | RevealAngle | — | Rotation transforms. |
| skewX, skewY | RevealAngle | — | Skew transforms. |
| perspective | RevealLength | — | Prepended as perspective(...) when provided. |
| raw | string | — | Appended to the generated transform string. |
Reveal exported types
| Type | Definition | Notes |
| ------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------- |
| RevealVariant | "fade" \| "transform" | Reveal animation mode. |
| RevealLength | number \| string | Numeric values resolve to pixel lengths. |
| RevealAngle | number \| string | Numeric values resolve to degree angles. |
| RevealMotionChannel | "opacity" \| "transform" | Channels used by duration and easing objects. |
| RevealChannelOptions<T> | Partial<Record<RevealMotionChannel, T>> | Per-channel option helper. |
| RevealDuration | number \| RevealChannelOptions<number> | Shared or per-channel duration. |
| RevealEasing | string \| RevealChannelOptions<string> | Shared or per-channel easing. |
| RevealTransformObject | transform fields table above | Object form for generated from-transforms. |
| RevealTransform | RevealTransformObject \| string | Object or raw CSS transform string. |
| RevealOptions | behavior options above | Options accepted by useReveal. |
| RevealProps<E> | RevealOptions plus polymorphic element props | Props accepted by <Reveal>. |
| UseRevealResult<T> | { ref, revealed, inView, revealProps } | Return value from useReveal; spread revealProps and attach ref to your element. |
Slider
The default Slider is the small synchronous core: children, drag, wheel navigation, snapping, grouping, looping, index channels, reveal, 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 are owned by SliderSkeleton; reload and back/forward restore lives in the opt-in RestoredSliderSkeleton, composed with useSliderReady().
import { Slider } from "react-motion-gallery/slider";
import { sliderArrows } from "react-motion-gallery/slider/arrows";
const slides = [
"https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=1600&h=900&q=80",
"https://images.unsplash.com/photo-1475924156734-496f6cac6ec1?auto=format&fit=crop&w=1600&h=900&q=80",
"https://images.unsplash.com/photo-1465146344425-f00d5f5c8f07?auto=format&fit=crop&w=1600&h=900&q=80",
];
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 reveal 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. |
| virtualization | SliderVirtualizationOptions | — | Windows large fixed-size horizontal slider tracks. Use the same object for fullscreen slider and thumbnail rails when all surfaces should window large media sets. |
| 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 \| number \| Record<string, number> | false | true groups each snap by the cells that fit in the viewport. A number groups exactly that many cells per snap without changing cell sizing. Responsive number maps use the same breakpoints prop as other slider responsive values. |
| 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. |
| scroll.containScroll | boolean | false | Clamps start/end snaps so non-looping variable-width or centered sliders do not leave excess empty space at the track edges. |
scroll.groupCells affects the snap pages used by drag, wheel, arrows, dots, and imperative navigation. Use true for automatic fit-to-viewport grouping, or a count for explicit snap pages:
<Slider
breakpoints={{ desktop: 1000 }}
scroll={{
groupCells: {
0: 1,
desktop: 3,
},
}}
>
{slides}
</Slider>Numeric values are truncated and clamped to the available slide count. 1, 0, negative numbers, NaN, and Infinity resolve to the normal ungrouped snap behavior, so the slider keeps its standard end-of-track snap handling. Responsive values are re-resolved on viewport resize.
Slider virtualization
Use virtualization when a slider has enough fixed-size cells that mounting every slide would be expensive. The same SliderVirtualizationOptions type is shared by base Slider, ThumbnailSlider, fullscreen slider, and FullscreenThumbnailSlider.
import { GalleryCore } from "react-motion-gallery/core";
import { Slider } from "react-motion-gallery/slider";
import { useFullscreenController } from "react-motion-gallery/fullscreen";
import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider";
import { sliderFullscreen } from "react-motion-gallery/slider/fullscreen";
const virtualization = {
enabled: true,
overscan: 3,
threshold: 40,
};
function FullscreenAddon() {
const { fullscreenNode } = useFullscreenController({
plugins: [fullscreenSlider()],
fullscreen: {
enabled: true,
slider: { virtualization },
},
});
return <>{fullscreenNode}</>;
}
export function LargeSlider({ slides }: { slides: Array<{ src: string }> }) {
return (
<GalleryCore layout="slider" fullscreenItems={slides.map((slide) => slide.src)}>
<Slider
layout={{ gap: 20, cellsPerSlide: { xs: 1, md: 3, lg: 4 } }}
scroll={{ groupCells: true, loop: true }}
virtualization={virtualization}
plugins={[sliderFullscreen()]}
>
{slides.map((slide, index) => (
<img key={slide.src} src={slide.src} alt={`Slide ${index + 1}`} />
))}
</Slider>
<FullscreenAddon />
</GalleryCore>
);
}| Option | Type | Default | Notes |
| ----------- | --------- | ------- | ----- |
| enabled | boolean | false | Enables fix
