Skip to content
v2.5.0
English
Русский
Documentation

IndexedSnapScroller

IndexedSnapScroller is a compound component built on SnapScroller for virtualized, index-windowed paging.
Month -1
Month 0
November 2026
December 2026
January 2027
indexed-snap-scroller.tsx · 11 lines
import { IndexedSnapScroller, IndexedSnapScrollerContent, useIndexedSnapScrollerContext } from '@vega-ui/react';

const IndexedSlide = () => {
  const { index } = useIndexedSnapScrollerContext();

  return <IndexedSnapScrollerContent>{MONTHS[index] ?? `Month ${index + 1}`}</IndexedSnapScrollerContent>;
};

<IndexedSnapScroller defaultIndex={0} tabIndex={0} aria-label='Months' style={{ width: 240, gap: 12 }}>
  <IndexedSlide />
</IndexedSnapScroller>
Browser support
Widely available
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari on iOS 15.4
Bundle size
Tested in
20 tests

Summary

IndexedSnapScroller is a compound component built on SnapScroller for virtualized, index-windowed paging. It maintains a sliding logical index sequence and can shift the window as the user reaches scroll boundaries.

Imports

import {
  IndexedSnapScroller,
  IndexedSnapScrollerContent,
  type IndexedSnapScrollerApiRef,
  type IndexedSnapScrollerContentProps,
  type IndexedSnapScrollerProps,
} from '@vega-ui/react';

Exported Types

  • IndexedSnapScrollerProps
  • IndexedSnapScrollerContentProps
  • IndexedSnapScrollerApiRef

Minimal Composition

minimal-composition.tsx · 40 lines
import {
  IndexedSnapScroller,
  IndexedSnapScrollerContent,
  useIndexedSnapScrollerContext,
} from '@vega-ui/react';

const today = new Date();
const monthFormat = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' });

function MonthPage() {
  const { index } = useIndexedSnapScrollerContext();
  const month = new Date(today.getFullYear(), today.getMonth() + index, 1);

Required Parts

  • IndexedSnapScroller: root indexed paging system
  • IndexedSnapScrollerContent: page content bound to the current virtual index

Optional Parts

  • apiRef for imperative index-window control

Composition Order

  1. IndexedSnapScroller
  2. one IndexedSnapScrollerContent child template

Variants

  • Controlled logical index
  • Uncontrolled windowed paging with defaultIndex
  • Adjustable size, shift, start, and startDir

Edge Cases

  • Controlled index and virtual window resets should stay synchronized.
  • preserveScroll changes how index-window shifts feel.
  • The children are re-rendered for each virtual index, so keyed state inside the page content must be intentional.

Common Mistakes

  • Assuming child order stays static even though the virtual window shifts.
  • Forgetting that one content template is rendered multiple times with different context indexes.
  • Mixing external controlled index logic with ad hoc internal offset assumptions.

Examples

Basic

Basic: infinite week strip

One WeekPage template is rendered for every index in the window and reads its own index from useIndexedSnapScrollerContext; index 0 is the current week. The date math lives in the page component, the scroller only supplies the index.

week-strip.tsx · 64 lines
import {
  IndexedSnapScroller,
  IndexedSnapScrollerContent,
  Text,
  useIndexedSnapScrollerContext,
} from '@vega-ui/react';
import type { CSSProperties } from 'react';

const today = new Date();
const daysSinceMonday = (today.getDay() + 6) % 7;
const weekdayFormat = new Intl.DateTimeFormat('en-US', { weekday: 'short' });
const weekFormat = new Intl.DateTimeFormat('en-US', { month: 'long', day: 'numeric' });

Controlled/Stateful

Controlled/Stateful: month header with external controls

index lives in the parent and onScrollSnapChange writes the settled index back. The arrows call apiRef.next() and prev() for a smooth step; +1 year and This month set index directly, which rebuilds the window around a target outside it and jumps there without animation.

controlled-virtual-index.tsx · 89 lines
import {
  Button,
  Icon,
  IconButton,
  IndexedSnapScroller,
  IndexedSnapScrollerContent,
  Text,
  useIndexedSnapScrollerContext,
  type IndexedSnapScrollerApiRef,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { useRef, useState } from 'react';

Form/Integration

Form/Integration: delivery day in a form

Only the pages of the current window exist in the DOM, so the selected day cannot live in a page: it is kept in form state and submitted through a hidden input read with FormData. Submitting without a day shows a HelperText linked to the group with aria-describedby; past days and Sundays are disabled buttons, and reset clears the value and returns to the current week with setIndexTo(0).

week-strip-form.tsx · 132 lines
import {
  Button,
  HelperText,
  Icon,
  IconButton,
  IndexedSnapScroller,
  IndexedSnapScrollerContent,
  Text,
  useIndexedSnapScrollerContext,
  type IndexedSnapScrollerApiRef,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';

Layout/Overlay

Layout/Overlay: month picker in a popover

Pages are years, each with twelve month buttons. The popover renders its content only while open, so defaultIndex reopens the picker on the year of the selected month; choosing a month closes the popover and updates the trigger label.

month-picker-in-popover.tsx · 90 lines
import {
  Button,
  IndexedSnapScroller,
  IndexedSnapScrollerContent,
  Popover,
  PopoverContent,
  PopoverTrigger,
  Text,
  useIndexedSnapScrollerContext,
} from '@vega-ui/react';
import { useState } from 'react';

Edge

Edge: bounded range

The window is infinite, so bounds are your job: indexes before the first statement and after the current month render placeholder pages, and when the scroll settles on one of them onScrollSnapChange scrolls back to the nearest real statement. The scroll back is scheduled with requestAnimationFrame because the scroller re-centers its window right after the callback.

bounded-statements.tsx · 89 lines
import {
  IndexedSnapScroller,
  IndexedSnapScrollerContent,
  Text,
  useIndexedSnapScrollerContext,
  type IndexedSnapScrollerApiRef,
} from '@vega-ui/react';
import { useRef, useState, type CSSProperties } from 'react';

const today = new Date();
const accountOpened = new Date(2025, 0, 1);
const firstIndex = (accountOpened.getFullYear() - today.getFullYear()) * 12 + accountOpened.getMonth() - today.getMonth();

Edge: lazy page data

Each page requests its data on mount and the parent caches results by index, so pages that leave the window and come back are not fetched again. size={7} and shift={3} keep more pages ready on both sides; onOffset fires every time the window grows at an edge.

lazy-page-data.tsx · 81 lines
import {
  IndexedSnapScroller,
  IndexedSnapScrollerContent,
  Text,
  useIndexedSnapScrollerContext,
} from '@vega-ui/react';
import { useCallback, useEffect, useRef, useState } from 'react';

const today = new Date();
const dayFormat = new Intl.DateTimeFormat('en-US', { weekday: 'long', month: 'short', day: 'numeric' });
const moneyFormat = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });

Edge: right-to-left months

Month names come from Intl.DateTimeFormat('ar-EG') and the container is dir='rtl': later months are on the left, next() scrolls to the left, and the arrow icons are swapped to match.

rtl-months.tsx · 59 lines
import {
  Icon,
  IconButton,
  IndexedSnapScroller,
  IndexedSnapScrollerContent,
  Text,
  useIndexedSnapScrollerContext,
  type IndexedSnapScrollerApiRef,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { useRef } from 'react';

Anatomy

Overview

IndexedSnapScroller wraps SnapScroller and generates a moving window of logical indexes. Each rendered child is wrapped in context that exposes its current virtual index.

Required Parts

IndexedSnapScroller

Required. Owns index-window generation, shift logic, controlled-index synchronization, and boundary offsets.

IndexedSnapScrollerContent

Required in normal composition. Binds one snapped page to the current virtual index from context.

Optional Parts

apiRef

Optional. Exposes the imperative indexed scroller API.

Composition Order

  1. IndexedSnapScroller
  2. one content template via IndexedSnapScrollerContent

Valid Composition Patterns

The child is a component, not markup: it calls useIndexedSnapScrollerContext and wraps its output in IndexedSnapScrollerContent. The scroller renders this one template once per index in the window.

minimal-composition.tsx · 40 lines
import {
  IndexedSnapScroller,
  IndexedSnapScrollerContent,
  useIndexedSnapScrollerContext,
} from '@vega-ui/react';

const today = new Date();
const monthFormat = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' });

function MonthPage() {
  const { index } = useIndexedSnapScrollerContext();
  const month = new Date(today.getFullYear(), today.getMonth() + index, 1);

Invalid Composition Patterns

Treating the content child as one fixed page

The child is really a template reused for multiple logical indexes.

External index logic that ignores virtual window resets

The controlled index and visible window drift apart.

Relying on stable physical DOM order for logical page identity

Window shifts can invalidate that assumption.