React Motion Gallery

Entry points

Choose the import path that matches the surface you are shipping.

Use the root export when one module needs several gallery surfaces. Prefer subpaths for routes or components that only need one surface, like 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.

import "react-motion-gallery/styles.css"; import { BREAKPOINT_MAP, Entries, FullscreenThumbnailSlider, GalleryCore, Grid, Masonry, Slider, ThumbnailSlider, Video, ZoomPanImage, createSliderIndexChannel, createThumbnailSyncBridge, flattenEntries, toMediaItems, useFullscreenController, useGalleryCore, useGridReady, useMasonryReady, useSliderReady, } from "react-motion-gallery";
CategoryEntry pointMain surfaceUse when
Stylesreact-motion-gallery/styles.csscompiled stylesheetRequired styles for gallery primitives and controls.
Utilitiesreact-motion-gallery/mediatoMediaItemsMedia normalization helpers.
Utilitiesreact-motion-gallery/responsiveBREAKPOINT_MAPShared breakpoint aliases for public responsive props.
Corereact-motion-gallery/coreGalleryCore, GalleryCoreProvider, useGalleryCoreFullscreen state, shared gallery context, and layout ownership.
Layoutreact-motion-gallery/sliderSlider, createSliderIndexChannelErgonomic slider with a small core and lazy extended behavior.
Layoutreact-motion-gallery/slider/readyuseSliderReadySlider readiness hook for stable skeleton and content reveal timing.
Sliderreact-motion-gallery/slider/arrowssliderArrowsArrow controls plugin for Slider navigation.
Sliderreact-motion-gallery/slider/dotssliderDotsDot navigation plugin for slide position and selection.
Sliderreact-motion-gallery/slider/progresssliderProgressProgress indicator plugin for Slider movement.
Sliderreact-motion-gallery/slider/scrollbarsliderScrollbarDraggable scrollbar plugin for Slider navigation.
Sliderreact-motion-gallery/slider/auto-heightsliderAutoHeightPlugin that sizes Slider height to the active slide.
Sliderreact-motion-gallery/slider/lazy-loadsliderLazyLoadSlider lazy media plugin for image shells, spinners, and reveal readiness.
Sliderreact-motion-gallery/slider/parallaxsliderParallaxParallax transform plugin for slide media or custom layers.
Sliderreact-motion-gallery/slider/scalesliderScaleScale effect plugin for active and neighboring slides.
Sliderreact-motion-gallery/slider/fadesliderFadeFade transition plugin for slide opacity changes.
Sliderreact-motion-gallery/slider/crossfadesliderCrossfadeCrossfade transition plugin for overlapping slides.
Sliderreact-motion-gallery/slider/fullscreensliderFullscreenSlider plugin that opens GalleryCore fullscreen from base slides.
Layoutreact-motion-gallery/gridGrid, Grid.ItemDirect-child CSS Grid layouts with responsive tracks and item spans.
Layoutreact-motion-gallery/grid/readyuseGridReadyGrid readiness hook for stable Skeleton wrapper handoff.
Layoutreact-motion-gallery/grid/lazy-loadgridLazyLoadGrid lazy media plugin for opt-in image shells and spinners.
Layoutreact-motion-gallery/masonryMasonry, Masonry.ItemMeasured uneven layouts with balanced or source-order placement.
Layoutreact-motion-gallery/masonry/readyuseMasonryReadyMasonry readiness hook for measured Skeleton wrapper handoff.
Layoutreact-motion-gallery/masonry/lazy-loadmasonryLazyLoadMasonry lazy media plugin for opt-in image shells and spinners.
Datareact-motion-gallery/entriesEntries, flattenEntries, entry media container helpersStructured editorial entries with coordinated text and media.
Loadingreact-motion-gallery/skeleton/baseSkeletonStandalone and wrapper skeletons that can mirror final layouts.
Loadingreact-motion-gallery/skeleton/sliderSliderSkeletonSlider-aware skeleton wrapper and measured initial-height helpers.
Loadingreact-motion-gallery/skeleton/gridGridSkeletonGrid-aware skeleton wrapper for responsive tracks and item slots.
Loadingreact-motion-gallery/skeleton/masonryMasonrySkeletonMasonry-aware skeleton wrapper for measured uneven layouts.
Fullscreenreact-motion-gallery/fullscreenuseFullscreenControllerSmall fullscreen controller hook. Add fullscreenSlider for the runtime.
Fullscreenreact-motion-gallery/fullscreen/sliderfullscreenSliderFullscreen slider runtime plugin.
Fullscreenreact-motion-gallery/fullscreen/controlsfullscreenControlsOption plugin for fullscreen close, arrows, and counter behavior.
Fullscreenreact-motion-gallery/fullscreen/captionsfullscreenCaptionsFullscreen caption runtime plugin.
Fullscreenreact-motion-gallery/fullscreen/zoom-panfullscreenZoomPanFullscreen click zoom, pan, and pinch runtime plugin.
Fullscreenreact-motion-gallery/fullscreen/videofullscreenVideoFullscreen Plyr video runtime plugin.
Fullscreenreact-motion-gallery/fullscreen/lazy-loadfullscreenLazyLoadFullscreen media lazy-load runtime plugin.
Fullscreenreact-motion-gallery/fullscreen/crossfadefullscreenCrossfadeFullscreen crossfade navigation option plugin.
Fullscreenreact-motion-gallery/fullscreen/thumbnailsfullscreenThumbnailsOption plugin for fullscreen thumbnail bridge behavior.
Navigationreact-motion-gallery/thumbnailsThumbnailSlider and thumbnail sync helpersSynced thumbnail rails for base sliders and gallery navigation.
Navigationreact-motion-gallery/fullscreenThumbnailsFullscreenThumbnailSliderThumbnail strips that connect directly to fullscreen state.
Mediareact-motion-gallery/videoVideoGallery-ready video rendering for HTML5, YouTube, and Vimeo.
Mediareact-motion-gallery/zoomPanZoomPanImageA clipped image zoom surface with drag, wheel, and pinch gestures.

Runtime Gzip Sizes

This table reports local gzip measurements for selected runtime surfaces. Type-only imports are erased and add no JS; the responsive row measures BREAKPOINT_MAP, and 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. If a surface creates async chunks, the row reports initial plus async JS. Run npm run build && npm run size:readme in packages/react-motion-gallery to refresh it.

SurfaceJS gzip
Entries13.1kB
FullscreenThumbnailSlider20.3kB
GalleryCore2.6kB
Grid6.3kB
grid/ready323.0B
grid/lazy-load3.3kB
Masonry6.5kB
masonry/ready323.0B
masonry/lazy-load3.3kB
Skeleton base8.1kB
skeleton/slider16.9kB
skeleton/grid10.4kB
skeleton/masonry17.8kB
Slider core18.7kB
slider/ready894.0B
slider/arrows1.2kB
slider/dots932.0B
slider/progress892.0B
slider/scrollbar1.2kB
slider/auto-height1.3kB
slider/lazy-load3.9kB
slider/parallax1.4kB
slider/scale1.2kB
slider/fade1.2kB
slider/crossfade2.8kB
slider/fullscreen959.0B
ThumbnailSlider18.9kB
useFullscreenController4.9kB
fullscreen/slider35.8kB
fullscreen/controls173.0B
fullscreen/captions13.1kB
fullscreen/zoom-pan9.9kB
fullscreen/video16.3kB
fullscreen/lazy-load13.1kB
fullscreen/crossfade181.0B
fullscreen/thumbnails160.0B
Video12.7kB
ZoomPanImage8.7kB
media / toMediaItems260.0B
responsive / BREAKPOINT_MAP85.0B

Overview

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

BASH snippet
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.

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

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.

TYPESCRIPT snippet
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.

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

OptionTypeDefaultNotes
childrenReact.ReactNodeThe gallery tree using the shared core.
layout"slider" | "grid" | "masonry" | "entries"Declares the owning base layout. Omit it for standalone fullscreen/core usage.
breakpointsRecord<string, number>xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536Breakpoint map shared with descendants.
fullscreenItemsMediaItem[] | string[][]Normalized fullscreen media list.
nodesReactNode | 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 / MethodTypeNotes
layout"slider" | "grid" | "masonry" | "entries" | nullCurrent owning layout, or null for standalone fullscreen/core usage.
effectiveBreakpointsRecord<string, number>Breakpoint map after merging custom GalleryCore.breakpoints with defaults.
normalizedItemsMediaItem[]Fullscreen item list normalized from fullscreenItems.
fsEnabledbooleantrue when a mounted fullscreen controller has enabled fullscreen behavior.
setFsEnabled(enabled: boolean) => voidEnables or disables fullscreen behavior. Usually handled by useFullscreenController.
isFullscreenOpenbooleantrue while fullscreen is open.
isFullscreenOpenRefReact.RefObject<boolean>Ref mirror for handlers that need the current fullscreen-open state.
setFullscreenOpen(open: boolean) => voidUpdates fullscreen-open state. Usually handled by the fullscreen runtime.
openFullscreenAt({ index, method?, event? }) => voidOpens fullscreen at a normalized fullscreen item index. Pass the source event for scale-origin detection.
notifyBaseVisibleIndex(index: number) => voidEmits the visible base media index for fullscreen lazy-load/prewarm coordination.
notifyFsVisibleIndex(index: number) => voidEmits the active fullscreen index back to base media.
registerExpandableImage(index: number, node: HTMLElement | null) => voidRegisters an origin surface for layoutless scale transitions.

ZoomPanImage

TYPESCRIPT snippet
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

TYPESCRIPT snippet
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.

OptionTypeDefaultNotes
layoutSkeletonNodeStructured placeholder layout tree.
childrenReact.ReactNodeReal content. When present, Skeleton renders content and loading layers.
readybooleanfalseReveals content and exits the skeleton once true.
enabledbooleantrueSet false to render content immediately with no skeleton layer.
forceboolean | { enabled?: boolean; showContent?: boolean; skeletonOpacity?: number }falseKeeps the skeleton visible. Set showContent: true to preview ready content under the skeleton, and tune the overlay with skeletonOpacity.
timing.exitMsnumber600Keeps the skeleton layer mounted for this long after exit starts and controls the opacity transition.
timing.minVisibleMsnumber220Minimum time the skeleton stays visible before exit can begin.
shellClassName / shellStylestring / CSSPropertiesWrapper-layer class and style for content+skeleton mode.
contentClassName / contentStylestring / CSSPropertiesContent-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.

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.

BASH snippet
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.

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().

TYPESCRIPT snippet
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

OptionTypeDefaultNotes
childrenReact.ReactNodeSlide content rendered in order.
initialIndexnumber0Selects the slide index used for the first layout and intro fade-in.
breakpointsRecord<string, number>xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536Merged with the internal breakpoint map for responsive values.
indexChannelSliderIndexChannelinternal channelShare index state with thumbnails or sibling sliders.
pluginsSliderPlugin[][]Explicit first-party slider features such as arrows, dots, auto-height, effects, fullscreen, or lazy-load.

Slider layout and scroll options

OptionTypeDefaultNotes
layout.gapnumber | Record<string, number>20Responsive gap between cells.
layout.cellsPerSlidenumber | 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.groupCellsbooleanfalseScrolls by grouped cells instead of every cell.
scroll.skipSnapsboolean | { enabled?: boolean; threshold?: number }falseAllows 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.strictSnapsbooleanfalsePrevents one drag release from settling more than one snap away from where the drag started. Overrides scroll.skipSnaps.
scroll.freeScrollbooleanfalseEnables free dragging instead of strict snapping.
scroll.loopbooleanfalseWraps 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.

OptionTypeDefaultNotes
elements.viewportElementStyleClass and inline style for the viewport element.
elements.containerElementStyleClass and inline style for the moving slider container.
transitions.intro.renderIntro({ active, containerProps }, content) => ReactNodeCustom intro wrapper.
transitions.intro.staggerMsnumberDelay between item fade-ins.
transitions.intro.durationMsnumberIntro fade duration.
transitions.intro.easingstringIntro 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.

TYPESCRIPT snippet
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>;
ImportFactoryNotes
react-motion-gallery/slider/arrowssliderArrows(options)Previous/next arrows.
react-motion-gallery/slider/dotssliderDots(options)Pagination dots.
react-motion-gallery/slider/progresssliderProgress(options)Progress bar or custom progress renderer.
react-motion-gallery/slider/scrollbarsliderScrollbar(options)Range-style position control.
react-motion-gallery/slider/ripplesliderRipple(options)Enables ripple feedback for controls that call createRipple.
react-motion-gallery/slider/auto-playsliderAutoPlay(options)Timed slide changes.
react-motion-gallery/slider/auto-scrollsliderAutoScroll(options)Timed continuous advancement.
react-motion-gallery/slider/auto-heightsliderAutoHeight(options)Measures active slide height and gates slider readiness until measured.
react-motion-gallery/slider/lazy-loadsliderLazyLoad(options)Adds lazy media attributes to slide images and videos.
react-motion-gallery/slider/parallaxsliderParallax(options)Parallax slide wrapper.
react-motion-gallery/slider/scalesliderScale(options)Scales non-active slides.
react-motion-gallery/slider/fadesliderFade(options)Fades non-active slides.
react-motion-gallery/slider/crossfadesliderCrossfade(options)Enables crossfade-aware control navigation.
react-motion-gallery/slider/fullscreensliderFullscreen()Bridges a GalleryCore layout="slider" slider to fullscreen.
react-motion-gallery/slider/loadingsliderLoading(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.

TYPESCRIPT snippet
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

FieldTypeNotes
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.
visibleCountnumber | Record<string, number>Responsive count of visible skeleton slots.
classNamestring | undefinedApplied to the skeleton overlay root.
styleReact.CSSProperties | undefinedInline styles for the skeleton overlay root.
layoutSliderSkeletonNode | undefinedStructured placeholder layout tree. Use kind: "slider" to model slide tracks.
backgroundColorstring | undefinedOverrides the shared skeleton background color token.
radiusnumber | string | undefinedOverrides the shared skeleton radius token.
shimmerSkeletonShimmer | undefinedShared shimmer settings for the entire skeleton tree.

SliderSkeletonSliderNode

FieldTypeNotes
kind"slider"Slider-specific skeleton layout root.
styleSkeletonContainerStyle | Record<string, SkeletonContainerStyle>Track-level container styles such as gap, padding, align, justify, width, and maxWidth.
countnumber | undefinedOptional explicit slot count for the layout. Falls back to visibleCount on the surrounding slider skeleton spec.
itemSkeletonNodeDefault placeholder node rendered in each slot.
itemWrapStyleSliderSkeletonWrapStyle | undefinedShared wrapper size, margin, border, and box-shadow rules for every slot. Border sizing is border-box.
slotsSliderSkeletonSlot[] | undefinedPer-slot overrides for variable widths, heights, aspect ratios, or custom placeholder nodes.
direction"row" | "col" | undefinedSlot flow direction. centering: "first" only affects row layouts.
childrenSkeletonNode[] | undefinedOptional extra skeleton content rendered after the slider row. It does not affect --rmg-slider-initial-height or reserve live layout space.

SliderSkeletonSlot

FieldTypeNotes
itemSkeletonNode | undefinedReplaces the base layout.item for one slot.
itemWrapStyleSliderSkeletonWrapStyle | undefinedMerges 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

OptionTypeDefaultNotes
motion.selectDurationnumber25Duration for snapped selection motion.
motion.freeScrollDurationnumber43Duration for free-scroll settling.
motion.frictionnumber0.68Drag and settling friction.

Slider render callback args

ArrowRenderArgs

FieldTypeNotes
refReact.RefObject<HTMLDivElement | null>Attach to the arrow root.
onClick() => voidCalls the built-in previous or next action.
hiddenbooleantrue when the arrow should not render visually.
disabledbooleantrue when navigation is unavailable.
createRipple(el: HTMLElement) => voidTriggers the built-in ripple effect manually.
classNamestring | undefinedResolved class name for the arrow root.

DotsRenderArgs

FieldTypeNotes
refReact.RefObject<HTMLDivElement | null>Attach to the dots root.
countnumberDot count.
activeIndexnumberCurrent selected slide index.
hiddenbooleantrue when dots should be hidden.
goTo(index: number) => voidNavigate to a slide.
getDotRef(index: number) => (el: HTMLDivElement | null) => voidRef factory for each dot.
createRipple(el: HTMLElement) => voidManual ripple trigger.
classNameContainerstring | undefinedResolved root class name.
classNameDotstring | undefinedResolved dot class name.

ProgressRenderArgs

FieldTypeNotes
refReact.Ref<HTMLDivElement>Attach to the progress root.
innerRefReact.Ref<HTMLDivElement> | undefinedAttach to the fill element.
hiddenbooleantrue when the progress bar should be hidden.
progressnumberProgress value from 0 to 1.
axis"x" | "y"Fill direction.
classNamestring | undefinedRoot class name.
styleReact.CSSProperties | undefinedRoot inline style.
innerClassNamestring | undefinedFill class name.
innerStyleReact.CSSProperties | undefinedFill inline style.

SliderHandle methods

MethodSignatureNotes
centerSlider() => voidRe-centers the slider after layout changes.
getIndex() => numberCurrent active slide index.
setIndex(i: number, mode?: IndexMode) => voidJumps or animates to a slide.
subscribeIndex(fn: () => void) => () => voidSubscribes to index changes.
slideIndexForCell(cellIndex: number) => numberMaps a cell index to its slide index when using grouped cells.
getRootNode() => HTMLElement | nullOuter slider root.
getContainerNode() => HTMLElement | nullMoving slide container.
getSlideNodes() => HTMLElement[]Current slide elements.
getViewportNode() => HTMLDivElement | nullScroll viewport.
onSlidesBuilt(cb: (nodes: HTMLElement[]) => void) => () => voidRuns when slide nodes are ready.
whenSlidesBuilt() => Promise<HTMLElement[]>Promise form of onSlidesBuilt.
isSlidesBuilt() => booleantrue once the slide list is ready.
onReady(cb: (nodes: HTMLElement[]) => void) => () => voidRuns when the slider has built, measured, committed its index, and all plugin ready gates have cleared.
whenReady() => Promise<HTMLElement[]>Promise form of onReady.
isReady() => booleantrue once the settled slider ready signal has fired.
scrollNext(mode?: IndexMode) => voidAdvances one step.
scrollPrev(mode?: IndexMode) => voidMoves backward one step.
canScrollNext() => booleanWhether next navigation is available.
canScrollPrev() => booleanWhether previous navigation is available.
scrollProgress() => numberCurrent progress from 0 to 1.
cellsInView() => number[]Canonical cell indexes currently visible.
append(nodes: ReactNode | ReactNode[]) => numberAppends nodes and returns the new total count.
prepend(nodes: ReactNode | ReactNode[]) => numberPrepends nodes and returns the new total count.
insert(index: number, nodes: ReactNode | ReactNode[]) => numberInserts nodes and returns the new total count.
remove(indexOrPredicate: number | ((i: number) => boolean)) => numberRemoves items and returns the new total count.
replace(index: number, node: ReactNode) => voidReplaces a node at an index.
setItems(nodes: ReactNode[]) => numberReplaces all nodes and returns the new total count.
onIndexChange(cb: (i: number, meta: { mode: IndexMode }) => void) => () => voidSubscribes to index changes.
getInternals() => { slides, slider, visibleImages, selectedIndex, sliderX, sliderVelocity, isWrapping }Low-level internals used by fullscreen and advanced sync code.

createSliderIndexChannel

TYPESCRIPT snippet
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> ); }
MethodSignatureNotes
createSliderIndexChannel(initialIndex = 0, initialMode = "animated") => SliderIndexChannelCreates a shared index event bus.
get() => { index: number; mode: IndexMode }Reads the stored index and mode.
set(next: number, mode?: IndexMode, opts?: { silent?: boolean }) => voidSets the current index and emits a "set" event unless silenced.
bump(delta: number, mode?: IndexMode, opts?: { silent?: boolean }) => voidEmits a relative index change event.
subscribe(fn: () => void) => () => voidSubscribes to channel updates.
onEvent(fn: (ev: IndexEvent) => void) => () => voidReceives the last "set" or "bump" event payload.
onBasePointerDown(fn: () => void) => () => voidSubscribes to base slider pointer-down events.
emitBasePointerDown() => voidBroadcasts 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.

TYPESCRIPT snippet
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

OptionTypeDefaultNotes
childrenReact.ReactNodeThumbnail nodes rendered in order. Overrides options.children when both are provided.
optionsThumbnailsOptionsBase thumbnail configuration object.
indexChannelSliderIndexChannelinternal channelShare the same channel as a base Slider to keep selection in sync.
breakpointsRecord<string, number>xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536Used to resolve layout.position and responsive loading counts.
onThumbnailClick(index: number) => voidFired when a thumbnail click publishes a selection to the shared channel.
onReadyChange(ready: boolean) => voidFired 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

OptionTypeDefaultNotes
childrenReact.ReactNodeFallback thumbnail content when component children are omitted.
layout.positionResponsivePosition"bottom"Thumbnail rail position: "top", "right", "bottom", or "left".
layout.gapnumber8Gap between thumbnails.
layout.centerbooleanfalseCenters the overall rail content within its container when possible.
layout.thumbnail.widthnumber | stringWidth for each thumbnail item.
layout.thumbnail.heightnumber | stringHeight for each thumbnail item.
layout.container.widthnumber | stringWidth for the outer thumbnail container.
layout.container.heightnumber | stringHeight for the outer thumbnail container.
scroll.freeScrollbooleantrueEnables drag or wheel movement without strict snapping.
scroll.groupCellsbooleanfalsePages the rail by grouped thumbnail cells.
scroll.loopbooleanfalseWraps thumbnails at the ends.
scroll.skipSnapsbooleanfalseAllows momentum to skip snap points.
scroll.centerActiveThumbbooleanfalseRepositions 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

OptionTypeDefaultNotes
elements.containerElementStyleClass and inline style for the outer thumbnail container.
elements.thumbnailElementStyleClass and inline style for each thumbnail item shell.
controls.enabledbooleanfalseShows previous and next arrows when the rail overflows.
controls.arrowElementStyleShared arrow class and style.
controls.prevElementStylePrevious-arrow override.
controls.nextElementStyleNext-arrow override.
controls.render(args: ArrowRenderArgs & { dir: "prev" | "next" }) => ReactNodeCustom renderer for both thumbnail arrows.
controls.renderPrev(args: ArrowRenderArgs) => ReactNodeCustom previous arrow.
controls.renderNext(args: ArrowRenderArgs) => ReactNodeCustom next arrow.
controls.ripple.enabledbooleantrueEnables ripple feedback for thumbnail arrows.
controls.ripple.classNamestringCustom ripple class for the arrow feedback element.
motion.selectDurationnumber25Duration for snapped thumbnail selection motion.
motion.freeScrollDurationnumber43Duration for free-scroll settling.
motion.frictionnumber0.68Drag and settling friction.
breakpointMapRecord<string, number>xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536Override map used for responsive thumbnail positions and loading counts.

Thumbnail transition options

OptionTypeDefaultNotes
transitions.loading.enabledbooleantrueEnables the thumbnail loading layer.
transitions.loading.forceboolean | { enabled?: boolean; showContent?: boolean; skeletonOpacity?: number }falseForces 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.skeletonCountnumber | 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.containerElementStyleClass and inline style for the built-in loading overlay container.
transitions.loading.elements.rowElementStyleClass and inline style for the built-in skeleton row or column wrapper.
transitions.loading.elements.thumbnailElementStyleClass and inline style for each built-in thumbnail placeholder.
transitions.loading.renderLoading({ count }) => ReactNodeReplaces the built-in thumbnail loading skeleton and receives the resolved responsive count.
transitions.loading.timing.exitMsnumber600Keeps the thumbnail loading layer mounted for this long after exit starts.
transitions.loading.timing.minVisibleMsnumber220Minimum time the loading layer stays visible before exit can begin.
transitions.intro.renderIntro({ active, containerProps }, inner) => ReactNodeCustom intro wrapper for the thumbnail rail.
transitions.intro.staggerMsnumber40Delay between thumbnail fade-ins.
transitions.intro.durationMsnumber300Intro fade duration.
transitions.intro.easingstring"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.

MethodSignatureNotes
createThumbnailSyncBridge(args: { localChannel, externalChannel?, clampIndex? }) => ThumbnailSyncBridgeCreates a bridge between local thumbnail state and an optional external slider channel.
start() => () => voidStarts syncing and returns a cleanup function.
stop() => voidStops syncing without disposing the channels.
publishThumbnailClick(index: number, mode?: IndexMode) => voidPublishes a thumbnail click to the external slider channel.

Grid

TYPESCRIPT snippet
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

OptionTypeDefaultNotes
childrenReact.ReactNodeGrid items rendered in order. Wrap individual cards in Grid.Item when they need custom spans or wrapper props.
breakpointsRecord<string, number>xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536Used to resolve responsive columns and gaps.
gridItemBaseClassstring"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.

OptionTypeDefaultNotes
childrenReact.ReactNodeThe grid card content.
spannumber | "full" | Record<string, number | "full">1Per-item track span. "full" renders grid-column: 1 / -1; numeric values render grid-column: span n / span n.
classNamestringExtra class name merged onto the grid item wrapper.
styleReact.CSSPropertiesInline styles merged onto the grid item wrapper.

Grid options

OptionTypeDefaultNotes
columnsnumber | Record<string, number>Fixed responsive column count. When omitted, Grid auto-fits using minColumnWidth.
templateColumnsstring | Record<string, string>Explicit grid-template-columns value. Takes precedence over columns and minColumnWidth.
minColumnWidthnumber | string160Minimum width used by auto-fit mode.
gapnumber | Record<string, number>8Responsive grid gap.
rootClassNamestringClass name for the grid root.
itemClassNamestringClass name added to each wrapped grid item.
fullscreenTrigger"item" | "media""media"Opens fullscreen from the clicked media node or the entire item shell.
pluginsGridPlugin[][]Explicit first-party Grid features such as lazy-load.
intro.renderIntro({ active, containerProps }, content) => ReactNodeCustom intro wrapper.
intro.staggerMsnumber60Reveal stagger for the fade-in.
intro.durationMsnumber600Intro fade duration.
intro.easingstring"cubic-bezier(.2,.7,.2,1)"Intro fade easing.
intro.staggerLimitnumberOptional cap on how many items stagger.

Grid plugins

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

TYPESCRIPT snippet
import { Grid } from "react-motion-gallery/grid"; import { gridLazyLoad } from "react-motion-gallery/grid/lazy-load"; <Grid plugins={[gridLazyLoad({ spinner: true })]}>{items}</Grid>;
ImportFactoryNotes
react-motion-gallery/grid/lazy-loadgridLazyLoad(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:

TYPESCRIPT snippet
<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:

TYPESCRIPT snippet
<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.

TYPESCRIPT snippet
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

TYPESCRIPT snippet
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

OptionTypeDefaultNotes
childrenReact.ReactNodeMasonry items rendered in order. Wrap individual cards in Masonry.Item when they need custom spans or wrapper props.
breakpointsRecord<string, number>xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536Used to resolve responsive columns and gaps.

Masonry.Item props

OptionTypeDefaultNotes
childrenReact.ReactNodeThe masonry card content.
spannumber | "full" | Record<string, number | "full">1Per-item track span. "full" resolves to the active column count and numeric values clamp to the current track count.
classNamestringExtra class name merged onto the masonry item wrapper.
styleReact.CSSPropertiesInline styles merged onto the masonry item wrapper.

Masonry options

OptionTypeDefaultNotes
columnsnumber | Record<string, number>Responsive column count.
gapnumber | 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.
itemWrapClassNamestringClass name added to the masonry item wrapper.
itemWrapStyleReact.CSSPropertiesInline styles applied to the masonry item wrapper.
asReact.ElementType"div"Root HTML element or custom component.
rootRefReact.Ref<HTMLDivElement>Ref to the masonry root.
classNames.rootstringRoot class name.
classNames.columnstringRetained for backwards compatibility with the legacy column-wrapper renderer.
classNames.itemstringItem class name.
pluginsMasonryPlugin[][]Explicit first-party Masonry features such as lazy-load.
intro.renderIntro({ active, containerProps }, content) => ReactNodeCustom intro wrapper.
intro.staggerMsnumber160Reveal stagger for the fade-in.
intro.durationMsnumber600Intro fade duration.
intro.easingstring"cubic-bezier(.2,.7,.2,1)"Intro fade easing.
intro.staggerLimitnumberOptional cap on how many items stagger.

Masonry plugins

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

TYPESCRIPT snippet
import { Masonry } from "react-motion-gallery/masonry"; import { masonryLazyLoad } from "react-motion-gallery/masonry/lazy-load"; <Masonry plugins={[masonryLazyLoad({ spinner: true })]}>{items}</Masonry>;
ImportFactoryNotes
react-motion-gallery/masonry/lazy-loadmasonryLazyLoad(options)Uses the same image shell behavior as Slider: trackable image src values move into data-rmg-lazy-src, real images load on intersection, and items fade in after decode and spinner exit.

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

Masonry already accepts arbitrary React children, including text-containing JSX. The wrapper props are only for styling the built-in masonry item shell.

Wrap a card in Masonry.Item when it needs its own span, wrapper className, or wrapper style:

TYPESCRIPT snippet
<Masonry columns={{ 0: 1, 760: 2, 1160: 4 }} gap={{ 0: 12, 1160: 18 }} placement="horizontalOrder" > <Masonry.Item span={{ 0: 1, 760: 2, 1160: 2 }}> <FeatureCard /> </Masonry.Item> <Masonry.Item span={1}> <StandardCard /> </Masonry.Item> </Masonry>

Choose a placement based on what should feel stable:

  • balanced: best when visual balance and the shortest overall columns matter most.
  • roundRobin: best when deterministic column assignment matters more than tight packing.
  • horizontalOrder: best when wider cards should still read in a mostly left-to-right order.

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

Masonry skeletons live in react-motion-gallery/skeleton/masonry and can use a structured layout spec with the same inner node vocabulary as Grid skeletons, including text nodes and itemWrapStyle.

Live Masonry content mounts invisibly until the current item set has completed an initial measurement pass. The Skeleton wrapper stays visible during that handoff, so the first revealed layout is based on measured DOM geometry rather than approximate height hints.

layout.slots gives Masonry the same per-card override escape hatch that slider skeletons have. Use a slot when one card needs a different placeholder tree, wrapper styling, span, or outer height. slot.span can override the corresponding Masonry.Item span for the placeholder, slot.ratio maps to Masonry's card-height rhythm, and slot.heightPx lets you pin a specific shell height when you need an exact placeholder.

TYPESCRIPT snippet
import { Masonry, useMasonryReady } from "react-motion-gallery"; import { MasonrySkeleton, type MasonrySkeletonSpec, } from "react-motion-gallery/skeleton/masonry"; const masonrySkeleton: MasonrySkeletonSpec = { ratios: [118, 126, 102, 146], layout: { kind: "masonry", itemWrapStyle: { padding: 14, borderRadius: 20, boxShadow: "0 18px 36px rgba(15, 23, 42, 0.08)", }, item: { kind: "col", style: { gap: 12 }, children: [ { kind: "rect", style: { width: "100%", height: 180, borderRadius: 16 }, }, { kind: "text", barHeight: 14, lineHeight: 1.55, lines: 3, lastBarWidth: "74%", style: { width: "100%" }, }, ], }, slots: [ { ratio: 182, span: { 0: 1, 1100: 2 }, item: { kind: "rect", style: { width: "100%", aspectRatio: "3 / 5", borderRadius: 16 }, }, }, ], }, }; function MasonryWithSkeleton({ items }: { items: React.ReactNode[] }) { const { ref: masonryRef, ready: masonryReady } = useMasonryReady(); return ( <MasonrySkeleton layout={masonrySkeleton} ready={masonryReady} timing={{ minVisibleMs: 220, exitMs: 600 }} masonry={{ count: items.length, columns: { 0: 1, 700: 2, 1100: 3 }, gap: { 0: 12, 1100: 20 }, placement: "balanced", }} > <Masonry ref={masonryRef} columns={{ 0: 1, 700: 2, 1100: 3 }} gap={{ 0: 12, 1100: 20 }} itemWrapStyle={{ padding: "6px", borderRadius: "28px", }} > {items} </Masonry> </MasonrySkeleton> ); }

Entries

Entries is the structured-data surface. You pass entry objects, render each media item however you want, and provide a renderMediaContainer function that decides whether an entry’s media should be laid out as a slider, grid, or masonry block.

TYPESCRIPT snippet
import * as React from "react"; import { Entries, GalleryCore, Slider, flattenEntries, type SliderHandle, } from "react-motion-gallery"; const entries = [ { id: "a", title: "Entry A", media: [ { kind: "image", src: "https://picsum.photos/seed/a1/1400/900", alt: "A1" }, { kind: "image", src: "https://picsum.photos/seed/a2/1400/900", alt: "A2" }, ], }, { id: "b", title: "Entry B", media: [{ kind: "image", src: "https://picsum.photos/seed/b1/1400/900", alt: "B1" }], }, ] as const; export function EntryGallery() { const flat = React.useMemo(() => flattenEntries(entries as any), []); const fullscreenItems = flat.flattenedMedia; return ( <GalleryCore layout="entries" fullscreenItems={fullscreenItems}> <Entries entries={{ items: entries as any, mediaLayout: "slider", render: { card: ({ entry, media }) => ( <article style={{ display: "grid", gap: 12 }}> <h3>{entry.title}</h3> {media} </article> ), media: ({ media, mediaIndex }) => media.kind === "image" ? ( <img key={mediaIndex} src={media.src} alt={media.alt ?? ""} style={{ width: "100%" }} /> ) : null, }, }} fullscreen={{ enabled: true }} renderMediaContainer={({ entryIndex, mediaNodes, entrySliderRefs }) => ( <Slider ref={(node: SliderHandle | null) => { if (entrySliderRefs?.current) entrySliderRefs.current[entryIndex] = node; }} > {mediaNodes} </Slider> )} /> </GalleryCore> ); }

Entry loading, decode, and reveal flow

When loading.enabled is true, entries use two viewport gates instead of one generic fade-in. loading.nearMargin marks a row as near the viewport, mounts the real entry content, and starts the entry media work early. loading.viewMargin and loading.threshold record when the row has actually entered view.

With loading.waitForDecode enabled, an entry does not reveal as soon as it intersects. The built-in gate waits for every trackable media URL in that entry to load and decode; in the current entry-level gate, that means image media in the entry’s media array. It falls back after loading.decodeTimeoutMs, and entries without image media are decode-ready immediately. The row fades from skeleton to content only after both conditions are true: the row has entered view and the entry media decode gate is ready.

Reveal timing is assigned when each entry becomes ready, so entries fade in by actual load/decode completion order as well as viewport intersection. A later row that loads quickly can take the next reveal slot while a slower row keeps its skeleton visible until its media is ready.

Fullscreen close has a matching entry-aware path. If the user closes fullscreen from a slide whose owning entry has not been viewed yet, the runtime resolves the flattened fullscreen index back to the owner entry, shows a temporary loading spinner while that row mounts and decodes, scrolls the owner entry into view, forces the skeleton/content layers to their final revealed state, and then runs the close animation back to the now-visible entry media. This keeps the close animation from landing on an unrevealed skeleton or an offscreen row.

Entries component props

OptionTypeDefaultNotes
enabledbooleantrueMaster switch for rendering entry content and transitions.
entriesEntriesOptionsStructured entry configuration.
fullscreen.enabledbooleantrueEnables fullscreen opening for entry media.
fullscreen.itemsMediaItem[] | string[]flattened entry mediaOptional fullscreen media override.
renderMediaContainer({ entryIndex, mediaNodes, entrySliderRefs }) => ReactNodeChooses how each entry’s media nodes are laid out.
nodeFromMedia(media: MediaItem) => ReactNodebuilt-in image/video rendererFallback renderer when entries.render.media is omitted.
entryFlatIndexRefReact.RefObject<number[][] | null>internal refReceives per-entry local-to-global media index maps.
entryMapRefReact.RefObject<MediaEntryLink[] | null>internal refReceives the flattened media-to-entry map.
fsOwnersRefReact.RefObject<SlideOwner[]>internal refReceives the fullscreen slide owner list.
entrySliderRefsReact.RefObject<(SliderHandle | null)[]>internal refLets renderMediaContainer wire fullscreen back to per-entry sliders.

EntriesOptions

OptionTypeDefaultNotes
itemsEntryItem[]Entry records. Each item can hold arbitrary fields plus media.
mediaLayout"slider" | "grid" | "masonry""slider"Declares the intended media layout.
render.card({ entry, entryIndex, media }) => ReactNodeWraps the media container in custom card UI.
render.media({ entry, entryIndex, media, mediaIndex }) => ReactNodeCustom media renderer per media item.
render.overlay({ entry, entryIndex, media, mediaIndex, link, opacity, fsIndex, style, containerProps }) => ReactNodeRenders fullscreen overlay content for the active entry slide.
render.skeleton({ entry, entryIndex }) => ReactNodeDeclared in the type, but the current runtime uses loading.skeleton instead.
overlayElementStyleStyles the fullscreen overlay container that wraps render.overlay.
overlay.overlayCrossfadeTarget"content" | "overlay""overlay"Selects whether fullscreen entry changes fade only the rendered overlay content or the whole overlay layer.
overlay.overlayCrossfadeDurationMsnumber300Duration for fullscreen entry overlay crossfades.
overlay.overlayCrossfadeEasingstring"cubic-bezier(.4,0,.22,1)"Easing for fullscreen entry overlay crossfades.
loading.enabledbooleanEnables entry loading and decode gating.
loading.forceboolean | { enabled?: boolean; showContent?: boolean; skeletonOpacity?: number }Forces entry skeletons to remain visible. Set showContent: true to preview mounted, ready entry content under the skeleton, and tune the loading overlay with skeletonOpacity.
loading.skeletonEntrySkeletonSpec | ((args) => EntrySkeletonSpec | null | undefined)Built-in skeleton spec or resolver.
loading.minHeightnumber | string"260px"Minimum reserved height while loading.
loading.nearMarginstring"700px 0px"Preload margin used before entries enter view.
loading.viewMarginstring"0px 0px"Margin used for the actual in-view gate.
loading.thresholdnumber0.01Intersection threshold for view detection.
loading.waitForDecodebooleantrueWaits for image decode before revealing an entry.
loading.decodeTimeoutMsnumber8000Decode timeout fallback.
loading.skeletonWrapElementStyleStyles the skeleton wrapper.
intro.renderIntro({ active, containerProps }, content) => ReactNodeCustom intro wrapper.
intro.staggerMsnumber200Delay between entry fade-ins.
intro.durationMsnumber700Entry intro fade duration.
intro.easingstring"cubic-bezier(.2,.7,.2,1)"Entry intro fade easing.
intro.staggerLimitnumber6Maximum number of entries that receive staggered delays.
entryListElementStyleStyles the entry list container.
entryRowElementStyleStyles each entry row container.

Entry skeleton text nodes also render wrapped line bars via lines, matching the slider and grid skeleton behavior, including responsive barHeight and line counts plus configurable trailing lastBarWidth.

Entry-related callback and helper types

EntryItem

FieldTypeNotes
mediaMediaItem[] | undefinedOptional list of media items for the entry.
[key: string]anyAdditional entry fields are allowed.

EntryMediaRenderArgs

FieldTypeNotes
entryEntryItemCurrent entry object.
entryIndexnumberEntry index.
mediaMediaItemCurrent media item.
mediaIndexnumberMedia index within the entry.

EntryCardRenderArgs

FieldTypeNotes
entryEntryItemCurrent entry object.
entryIndexnumberEntry index.
mediaReactNodeThe rendered media container returned by renderMediaContainer.

EntryOverlayRenderArgs

FieldTypeNotes
entryEntryItemEntry owning the active fullscreen slide.
entryIndexnumberEntry index.
mediaMediaItem | nullMedia item for the active fullscreen slide, when available.
mediaIndexnumber | nullMedia index inside the entry when available.
linkMediaEntryLink | nullFlattened link back to the entry/media pair.
opacitynumberOverlay opacity supplied by the runtime.
fsIndexnumberCurrent fullscreen slide index.
styleReact.CSSPropertiesOverlay positioning and animation style.
containerPropsReact.HTMLAttributes<HTMLDivElement>Props to spread onto the overlay root.

EntrySkeletonRenderArgs

FieldTypeNotes
entryEntryItemCurrent entry object.
entryIndexnumberEntry index.
FieldTypeNotes
entryIndexnumberEntry index.
mediaIndexnumberMedia index inside the entry.

SlideOwner

FieldTypeNotes
entryIndexnumberEntry that owns a fullscreen slide.

flattenEntries

FieldTypeNotes
flattenedMediaMediaItem[]One flat media array, in fullscreen order.
flattenedMapMediaEntryLink[]Global slide index back to entryIndex and mediaIndex.
entryFlatIndexnumber[][] | nullPer-entry lookup from local media index to global slide index.
ownersSlideOwner[]Owner metadata for each flattened slide.

Fullscreen

Fullscreen is compositional. GalleryCore owns the normalized fullscreen item list, your layout opens slides through that core, and useFullscreenController renders the portal UI.

Standalone fullscreen

Use GalleryCore without a layout prop when your own markup owns the visible surface. Call openFullscreenAt with the matching item index, and render the fullscreen portal once inside the core.

TYPESCRIPT snippet
import * as React from "react"; import { GalleryCore, useGalleryCore } from "react-motion-gallery/core"; import { useFullscreenController } from "react-motion-gallery/fullscreen"; import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider"; import { toMediaItems } from "react-motion-gallery/media"; const images = [ { src: "https://picsum.photos/id/1015/1600/900", alt: "Mountain lake", }, { src: "https://picsum.photos/id/1018/1600/900", alt: "Forest path", }, ]; const fullscreenItems = toMediaItems(images); function FullscreenPortal() { const { fullscreenNode } = useFullscreenController({ plugins: [fullscreenSlider()], fullscreen: { enabled: true }, }); return <>{fullscreenNode}</>; } function ImageButton(props: { image: (typeof images)[number]; index: number; }) { const gallery = useGalleryCore(); const open = (event: React.MouseEvent<HTMLButtonElement>) => { gallery.openFullscreenAt({ index: props.index, event: event.nativeEvent, }); }; return ( <button type="button" onClick={open}> <img src={props.image.src} alt={props.image.alt} style={{ display: "block", width: 180, aspectRatio: "16 / 9", objectFit: "cover", }} /> </button> ); } export function StandaloneFullscreen() { return ( <GalleryCore fullscreenItems={fullscreenItems}> {images.map((image, index) => ( <ImageButton key={image.src} image={image} index={index} /> ))} <FullscreenPortal /> </GalleryCore> ); }

Slider fullscreen

TYPESCRIPT snippet
import * as React from "react"; import { GalleryCore, Slider, useFullscreenController } from "react-motion-gallery"; import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider"; const slides = [ "https://picsum.photos/id/1015/1600/900", "https://picsum.photos/id/1018/1600/900", "https://picsum.photos/id/1024/1600/900", ]; function FullscreenAddon() { const { fullscreenNode } = useFullscreenController({ plugins: [fullscreenSlider()], fullscreen: { enabled: true }, }); return <>{fullscreenNode}</>; } export function SliderWithFullscreen() { return ( <GalleryCore layout="slider" fullscreenItems={slides}> <Slider> {slides.map((src, index) => ( <img key={src} src={src} alt={`Slide ${index + 1}`} style={{ width: "100%" }} /> ))} </Slider> <FullscreenAddon /> </GalleryCore> ); }

Fullscreen lazy-load handshake

Fullscreen keeps the base layout and fullscreen surface as separate render trees joined by one canonical index. The base layout can render thumbnails, cropped images, cards, or entries while GalleryCore.fullscreenItems provides the media that fullscreen renders for the same positions.

That index is also the communication channel for lazy loading. When a base item becomes visible, GalleryCore emits a base-visible event. If fullscreen.lazyLoad.images.enabled or fullscreen.lazyLoad.videos.enabled is active through fullscreenLazyLoad(), the fullscreen runtime listens for that event and prewarms the matching fullscreen media: images are fetched and decoded with high priority, and videos can prewarm their poster/source before being force-mounted.

Once the modal is open, the fullscreen slider index becomes the live gate. fsSub changes recompute which canonical image or video is allowed to mount or apply its source, then notify the lazy slide listeners. The active slide is always allowed; decoded images and prepared videos stay warm, and videos that were prewarmed from the base layout remain in the allowed set so navigation can land on prepared media.

Fullscreen also emits its visible index back through GalleryCore. Base media primitives use the core fullscreen state to suspend while fullscreen is active, and can use the visible fullscreen index to prewarm their matching media. Captions, overlays, and thumbnail rails stay synchronized through the same index contract.

For custom fullscreen images, fullscreen.renderImage must render a real descendant <img>. With fullscreenLazyLoad({ images: { enabled: true } }), that custom renderer participates in the same mount, spinner, load, and decode flow instead of mounting every fullscreen image eagerly.

Add fullscreen thumbnails by rendering FullscreenThumbnailSlider with the bridge returned from useFullscreenController.

TYPESCRIPT snippet
import { FullscreenThumbnailSlider, useFullscreenController } from "react-motion-gallery"; import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider"; import { fullscreenThumbnails } from "react-motion-gallery/fullscreen/thumbnails"; function FullscreenWithThumbs({ thumbs }: { thumbs: string[] }) { const { fullscreenNode, fullscreenThumbnailBridge } = useFullscreenController({ plugins: [fullscreenSlider(), fullscreenThumbnails()], fullscreen: { enabled: true, slider: { direction: "rtl", }, }, }); return ( <> {fullscreenNode} <FullscreenThumbnailSlider bridge={fullscreenThumbnailBridge} items={thumbs.map((thumbSrc, index) => ({ thumbSrc, alt: `Thumb ${index + 1}` }))} position="bottom" thumbnailHeight={60} gap={10} /> </> ); }

Set fullscreen.slider.direction when fullscreen should mirror RTL interaction:

TYPESCRIPT snippet
useFullscreenController({ plugins: [fullscreenSlider()], fullscreen: { enabled: true, slider: { direction: "rtl", }, }, });

Set fullscreen.slider.gap to add space between fullscreen slides. It accepts the same responsive number form as the base slider, using the GalleryCore.breakpoints map for named breakpoint keys:

TYPESCRIPT snippet
useFullscreenController({ plugins: [fullscreenSlider()], fullscreen: { enabled: true, slider: { gap: { 0: 12, md: 20, 1200: 28 }, }, }, });

Import fullscreenVideo from react-motion-gallery/fullscreen/video for fullscreen video slides. Set fullscreen.video.playOnOpen to start a Plyr-backed fullscreen video when fullscreen opens directly onto that video slide:

TYPESCRIPT snippet
useFullscreenController({ plugins: [fullscreenSlider(), fullscreenVideo()], fullscreen: { enabled: true, video: { playOnOpen: true, }, }, });

useFullscreenController args

OptionTypeDefaultNotes
pluginsFullscreenPlugin[][]Explicit first-party fullscreen features. At minimum, import fullscreenSlider() to mount the fullscreen runtime.
fullscreenFullscreenOptionsFullscreen behavior and rendering options.
ImportFactoryNotes
react-motion-gallery/fullscreen/sliderfullscreenSlider(options)Mounts the fullscreen slider runtime and accepts fullscreen.slider options.
react-motion-gallery/fullscreen/controlsfullscreenControls(options)Option plugin for close, arrows, and counter options. Use with fullscreenSlider().
react-motion-gallery/fullscreen/captionsfullscreenCaptions(options)Adds caption rendering, placement, and caption motion runtime. Use with fullscreenSlider().
react-motion-gallery/fullscreen/zoom-panfullscreenZoomPan(options)Adds fullscreen click zoom, pan, and pinch runtime. Use with fullscreenSlider().
react-motion-gallery/fullscreen/videofullscreenVideo(options)Adds fullscreen Plyr rendering, source/options, and playOnOpen runtime. Use with fullscreenSlider().
react-motion-gallery/fullscreen/lazy-loadfullscreenLazyLoad(options)Adds fullscreen image and video lazy-load gates. Use with fullscreenSlider().
react-motion-gallery/fullscreen/crossfadefullscreenCrossfade(options)Option plugin for fullscreen crossfade controls, drag, and wheel behavior. Use with fullscreenSlider().
react-motion-gallery/fullscreen/thumbnailsfullscreenThumbnails()Option-only plugin for fullscreen thumbnail bridge behavior. Use with fullscreenSlider().
FieldTypeNotes
fullscreenNodeReactNodeThe fullscreen portal UI. Render this once inside the GalleryCore tree.
fullscreenThumbnailBridgeFullscreenThumbnailBridgeBridge consumed by FullscreenThumbnailSlider.
openFullscreenAt(source, index, originEl?, requestedMethod?) => voidProgrammatic fullscreen open helper returned by the controller.
showFullscreenModalbooleantrue while the fullscreen modal is mounted and open.
showFullscreenSliderbooleantrue once the slider portion is visible.
fsFadeOpeningbooleantrue while a fade-based open animation is running.
closingModalbooleantrue while the close animation is running.

The hook returns additional refs and setters for the internal fullscreen runtime. Those values are implementation plumbing and are not the recommended consumer-facing surface for app code.

FullscreenOptions

OptionTypeDefaultNotes
enabledbooleanfalseMaster switch for fullscreen UI.
itemsMediaItem[] | string[]Declared in the type, but current fullscreen media resolution comes from GalleryCore.fullscreenItems.
renderImage({ item, index, isZoomed, className, baseStyle }) => ReactNodeCustom fullscreen image renderer. Must render a real descendant <img>. With lazyLoad.images.enabled, the renderer is mounted only when the slide is allowed and the runtime watches that descendant image for load/decode readiness.
video.source(item: MediaItem, index: number) => Plyr.SourceInfoBuilds fullscreen Plyr sources for video items.
video.optionsPlyr.Options | ((item: MediaItem, index: number) => Plyr.Options)Builds fullscreen Plyr options.
video.playOnOpenbooleanfalseAttempts to play the fullscreen Plyr video when fullscreen opens directly onto a video slide. Browser autoplay rules still apply.
video.styleReact.CSSPropertiesFullscreen player inline style.
video.classNamestringFullscreen player class.
controls.close.enabledbooleantrueToggles the close button.
controls.close.styleReact.CSSProperties{}Close button inline style.
controls.close.classNamestring""Close button class.
controls.close.render() => ReactNodeCustom close button renderer.
controls.arrows.enabledbooleantrueToggles fullscreen arrows.
controls.arrows.arrowElementStyle{}Shared arrow style.
controls.arrows.prevElementStyle{}Previous-arrow override.
controls.arrows.nextElementStyle{}Next-arrow override.
controls.arrows.render({ dir }) => ReactNodeCustom renderer for both arrows.
controls.arrows.renderPrev() => ReactNodeCustom previous arrow.
controls.arrows.renderNext() => ReactNodeCustom next arrow.
controls.counter.enabledbooleantrueToggles the index counter.
controls.counter.styleReact.CSSProperties{}Counter inline style.
controls.counter.classNamestring""Counter class.
controls.counter.render({ index, count }) => ReactNodeCustom counter renderer.
caption.classNamestringCaption root class.
caption.styleReact.CSSPropertiesCaption root style.
caption.placement"top" | "right" | "bottom" | "left"Preferred caption placement.
caption.widthnumberCaption area width.
caption.heightnumberCaption area height.
caption.breakpointnumberViewport cutoff for switching placement logic.
caption.render({ item, index, isZoomed }) => ReactNodeCustom caption renderer.
caption.layout"overlay" | "slide"Chooses whether the caption overlays the media or lives in the slide layout.
caption.overlayCrossfadeTarget"content" | "overlay""content"Selects whether overlay caption changes fade only the rendered caption content or the whole overlay layer.
caption.overlayCrossfadeDurationMsnumber300Duration for fullscreen overlay caption crossfades.
caption.overlayCrossfadeEasingstring"cubic-bezier(.4,0,.22,1)"Easing for fullscreen overlay caption crossfades.
caption.zoomFadebooleantrueFades captions out on fullscreen zoom-in and back in on zoom-out.
caption.zoomFadeDurationMsnumber300Duration for fullscreen caption zoom fades.
caption.zoomFadeEasingstring"cubic-bezier(.4,0,.22,1)"Easing for fullscreen caption zoom fades.
caption.zoomInTransformstring""Optional transform applied while captions fade out on zoom-in.
caption.zoomOutTransformstring""Optional transform used as the starting point when captions fade back in on zoom-out.
slider.durationnumber25Fullscreen slider motion duration.
slider.frictionnumber0.68Fullscreen slider friction.
slider.direction"ltr" | "rtl""ltr"Fullscreen slider interaction direction.
slider.gapnumber | Record<string, number>0Responsive pixel gap between fullscreen slides. Named keys resolve from GalleryCore.breakpoints.
zoom.clickZoomLevelnumber2.5Zoom level used for click-to-zoom.
zoom.maxZoomLevelnumber3Maximum allowed zoom level.
zoom.panDurationnumber43Pan settling duration.
zoom.panFrictionnumber0.68Pan friction.
effects.introDurationnumber300Open animation duration.
effects.introEasingstring"cubic-bezier(.4,0,.22,1)"Open animation easing.
effects.introFadebooleanfalseForces fade intro behavior.
effects.crossfade.controlsbooleanfalseUses crossfade transitions for fullscreen arrow navigation and animated slide requests. Also enables wheel crossfade unless effects.crossfade.wheel is provided.
effects.crossfade.dragbooleanfalseScrubs adjacent fullscreen slides with crossfade during drag instead of moving the track.
effects.crossfade.wheelboolean | CrossFadeWheelOptionseffects.crossfade.controlsUses wheel or touchpad travel as a one-slide-at-a-time fullscreen crossfade gesture. Set false to keep arrow crossfades while using normal wheel scrolling.
effects.crossfade.wheel.enabledbooleantrue when object form is usedEnables or disables fullscreen wheel crossfade when using the object form.
effects.crossfade.wheel.sensitivitynumber5Multiplies wheel delta into virtual drag progress. Higher values reach the commit threshold sooner.
effects.crossfade.wheel.commitThresholdnumber0.38Progress needed to commit to the previous or next fullscreen slide. Values are clamped from 0 to below 0.5.
effects.crossfade.wheel.durationMsnumbereffects.crossfade.durationMsFade duration after fullscreen wheel crossfade commits.
effects.crossfade.wheel.sessionGapMsnumber24Short quiet window used to distinguish same-direction touchpad tail from a fresh fullscreen wheel gesture after a committed wheel crossfade.
effects.crossfade.durationMsnumber120Shared fullscreen crossfade duration for controls, drag release, and wheel commit unless wheel overrides it.
effects.crossfade.easingstring"cubic-bezier(.4,0,.22,1)"Shared fullscreen crossfade easing.
lazyLoad.images.enabledbooleanEnables fullscreen image lazy loading. Base-visible indices predecode matching fullscreen images, and fullscreen index changes allow the active image slide to mount or apply its source.
lazyLoad.images.spinnerboolean | ReactNode | ((args) => ReactNode)Spinner override for fullscreen images.
lazyLoad.images.spinnerClassNamestringSpinner class for image slides.
lazyLoad.images.spinnerStyleReact.CSSPropertiesSpinner style for image slides.
lazyLoad.videos.enabledbooleanOpts fullscreen videos into lazy mounting. Base-visible indices prewarm matching video posters/sources and fullscreen index changes mount the active or already-prepared video slide. By default fullscreen Plyr videos mount eagerly in the hidden fullscreen tree.
lazyLoad.videos.spinnerboolean | ReactNode | ((args) => ReactNode)Spinner override for fullscreen videos.
lazyLoad.videos.spinnerClassNamestringSpinner class for video slides.
lazyLoad.videos.spinnerStyleReact.CSSPropertiesSpinner style for video slides.

Fullscreen effects.crossfade.wheel uses the same true, false, or object form as slider wheel crossfade. Its durationMs default follows fullscreen effects.crossfade.durationMs, which defaults to 120.

Fullscreen callback and helper types

FsCounterArgs

FieldTypeNotes
indexnumberCurrent fullscreen index.
countnumberTotal slide count.

FsCaptionRenderArgs

FieldTypeNotes
itemMediaItemActive fullscreen item.
indexnumberActive fullscreen index.
isZoomedbooleantrue when the active slide is zoomed.

FsCaptionPlacement

ValueNotes
"top"Places the caption above the media.
"right"Places the caption to the right of the media.
"bottom"Places the caption below the media.
"left"Places the caption to the left of the media.

FsIntroRequest

FieldTypeNotes
originalImageHTMLImageElement | nullOrigin image used for scale transitions.
indexnumberTarget fullscreen index.
method"fade" | "scale"Requested intro method.
closestSelectorstring | undefinedSelector used to resolve the source slide element.

FullscreenLazyLoadArgs

FieldTypeNotes
kind"image" | "video"Media kind currently loading.
isCloneboolean | undefinedtrue for cloned looped slides when relevant.

FullscreenThumbnailSlider props

FullscreenThumbnailSliderProps is exported from both the package root and react-motion-gallery/fullscreenThumbnails. The table below summarizes the prop surface.

OptionTypeDefaultNotes
bridgeFullscreenThumbnailBridgeBridge returned from useFullscreenController.
items{ thumbSrc: string; alt?: string }[]Thumbnail list.
position"top" | "right" | "bottom" | "left"Thumbnail rail position.
containerClassNamestringThumbnail container class.
containerStyleReact.CSSPropertiesThumbnail container style.
thumbnailWidthnumber | stringIndividual thumbnail width.
thumbnailHeightnumber | stringIndividual thumbnail height.
thumbnailsCenterbooleanCenters the thumbnail strip within its container.
thumbnailsContainerWidthnumber | stringExplicit strip width.
thumbnailsContainerHeightnumber | stringExplicit strip height.
fadeDurationMsnumber300Mount and unmount fade duration.
fadeEasingstring"cubic-bezier(.4,0,.22,1)"Fade easing.
thumbnailItemClassNamestringThumbnail item class.
thumbnailItemStyleReact.CSSPropertiesThumbnail item style.
gapnumberGap between thumbnails.
freeScrollbooleanEnables free thumbnail dragging.
groupCellsbooleanGroups thumbnail cells into snaps.
loopbooleanLoops the thumbnail slider.
axis"x" | "y"Declared in the prop type, but the current implementation does not wire it through.
skipSnapsbooleanAllows momentum to skip snaps.
centerActiveThumbbooleanKeeps the active thumbnail centered.
selectDurationnumberSelection motion duration.
freeScrollDurationnumberFree-scroll settling duration.
sliderFrictionnumberThumbnail slider friction.
breakpointMapRecord<string, number>{ xs: 0, sm: 640, md: 768, lg: 1024, xl: 1280 }Breakpoints used by the thumbnail strip.
rippleEnabledbooleanEnables thumbnail arrow ripples.
rippleClassNamestringRipple class name.
showArrowsbooleanfalseToggles thumbnail arrows.
arrowStylesReact.CSSPropertiesShared arrow styles.
arrowClassNamestringShared arrow class.
prevArrowStylesReact.CSSPropertiesPrevious-arrow styles.
prevArrowClassNamestringPrevious-arrow class.
nextArrowStylesReact.CSSPropertiesNext-arrow styles.
nextArrowClassNamestringNext-arrow class.
renderArrows(args) => ReactNodeCustom renderer for both arrows.
renderPrevArrow(args) => ReactNodeCustom previous arrow.
renderNextArrow(args) => ReactNodeCustom next arrow.

FullscreenThumbnailBridge

FieldTypeNotes
mountElHTMLDivElement | nullPortal mount node for the thumbnail strip.
fsSubFullscreenSliderSubFullscreen slider index channel used internally.
visiblebooleantrue when the strip should be visible.
invisiblebooleantrue during hidden transitional states.
direction"ltr" | "rtl"Fullscreen direction.
registerLayout(layout: FullscreenThumbnailSlotLayout) => voidRegisters the slot layout metadata.
clearLayout() => voidClears the current slot layout.

FullscreenThumbnailSlotLayout

FieldTypeNotes
position"top" | "right" | "bottom" | "left"Thumbnail rail position.
classNamestring | undefinedSlot container class.
styleReact.CSSProperties | undefinedSlot container style.
fadeDurationMsnumber | undefinedSlot fade duration.
fadeEasingstring | undefinedSlot fade easing.

Video

Video is the gallery-aware video primitive. It mounts Plyr lazily, syncs with gallery visibility, and can be used inside Slider, Grid, Masonry, Entries, and fullscreen flows.

TYPESCRIPT snippet
import { Video } from "react-motion-gallery"; export function BasicVideo() { return ( <div style={{ width: "100%", aspectRatio: "16 / 9", overflow: "hidden" }}> <Video src="https://cdn.plyr.io/static/blank.mp4" poster="https://picsum.photos/seed/video-poster/1600/900" options={{ controls: ["play", "progress", "mute", "fullscreen"] } as any} lazyLoad={{ enabled: true, spinner: true }} /> </div> ); }

Video props

VideoProps is exported from both the package root and react-motion-gallery/video. The table below summarizes the prop surface.

OptionTypeDefaultNotes
srcstringSource URL used to build the default Plyr source.
posterstringPoster image.
altstringOptional metadata label; the Video component itself does not render a visible alt attribute.
sourcePlyr.SourceInfoauto-built MP4 sourceDirect Plyr source object. Overrides sourceBuilder.
sourceBuilder({ src: string }) => Plyr.SourceInfoBuilds the Plyr source from src.
optionsPlyr.Options | (({ src, index }) => Plyr.Options)Direct or computed Plyr options. When omitted, the component still applies autoplay: false and preload: "none" defaults internally.
classNamestringPlayer wrapper class.
styleReact.CSSPropertiesPlayer wrapper style.
onApi(api: APITypes | null) => voidCalled whenever the Plyr API ref changes.
registerApiByIndex(index: number, api: APITypes | null) => voidRegisters the API by canonical gallery index.
lazyLoad.enabledbooleantruefalse mounts immediately after reveal.
lazyLoad.spinnerboolean | ReactNode | ((args) => ReactNode)truefalse disables the spinner; true uses the built-in spinner.
lazyLoad.spinnerClassNamestringSpinner wrapper class.
lazyLoad.spinnerStyleReact.CSSPropertiesSpinner wrapper style.

Supporting video types

These helper type names are available from both the package root and react-motion-gallery/video.

TypeShapeNotes
RmgPlyrSourceBuilder({ src: string }) => Plyr.SourceInfoUsed by sourceBuilder.
RmgPlyrOptionsResolverPlyr.Options | (({ src, index }) => Plyr.Options)Used by options.
RmgVideoLazyLoadOptions{ enabled?, spinner?, spinnerClassName?, spinnerStyle? }Used by lazyLoad.

If you do not use Video, you do not need plyr or plyr-react. Install those optional peer dependencies only for video playback.