Горизонтальное меню, которое знает, что видно

Меню прокрутки для React на собственной прокрутке браузера — отслеживание видимости по элементам, стрелки, перетаскивание и полный императивный API. 5,7 КБ в gzip.

Sci-Fi
Comedy
Drama
Horror
Docs
Kids
Action
Indie
Anime
Classics
Thriller
Reality

getVisible() → ['scifi', 'comedy', 'drama', 'horror', 'docs', 'kids']

This is the library, live — drag it. Dimmed tiles are the ones useIsVisible reports as off-screen.

npm install react-horizontal-scrolling-menu347 тыс. загрузок/мес5,7 КБ min+gzipReact 16.8 – 19MIT

Автовоспроизведение без движка карусели

Пропа autoplay нет — этот рельс является рецептом на публичном API: ряд, склонированный на оба конца, один прыжок scrollLeft на шве и таймер, вызывающий scrollNext(). Он ставится на паузу при наведении, фокусе и на скрытых вкладках, неподвижен при уменьшенной анимации — и его можно перетаскивать, даже назад, через шов.

Tokyo
Oslo
Lima
Cairo
Sydney
Quito
Seoul
Porto
Denver
Hanoi

Это меню, а не карусель

Embla, Swiper и keen-slider заново реализуют прокрутку на JavaScript, чтобы строить слайдеры изображений — точки снапа, пружинная физика, цикл рендера. Эта библиотека не поставляет ничего из этого. Она едет на нативной прокрутке браузера и добавляет то, чего браузер не даёт: точное знание, какие элементы на экране.

Неправильный инструмент для полноэкранного слайдера изображений — там используйте Embla или Swiper. Правильный инструмент для строк категорий, полос вкладок, чип-фильтров и любого ряда вещей, который вашему приложению нужно отслеживать.

Нативная прокрутка

Инерция, полоса прокрутки, касание, колесо и доступность берутся из браузера, а не из физического движка. Ряд прокручивается до того, как ваш JavaScript гидрируется, — каждая демонстрация на этой странице рендерится на сервере.

Отслеживание видимости

IntersectionObserver сообщает, какие элементы на экране. useIsVisible(itemId) подписывает один компонент на один элемент — никакой математики позиций прокрутки, и перерисовываются только затронутые элементы.

Императивность, когда нужно

scrollToItem, scrollNext, scrollPrev, поиск по id или индексу — через контекст внутри меню или apiRef снаружи.

Ваши компоненты, ваш CSS

Стрелки, заголовок, футер и каждый элемент — это компоненты, которые пишете вы. Ширина элемента — ваш CSS. Библиотека поставляет 210 байт стилей раскладки и не мешается.

Быстрый старт

Один файл, без настройки: элементы с itemId, две стрелки, читающие VisibilityContext, и импорт таблицы стилей.

App.tsx
import React from 'react';
import {
  ScrollMenu,
  VisibilityContext,
  type publicApiType,
} from 'react-horizontal-scrolling-menu';
import 'react-horizontal-scrolling-menu/dist/styles.css';

const items = Array.from({ length: 10 }, (_, i) => `item-${i + 1}`);

export function App() {
  return (
    <ScrollMenu LeftArrow={LeftArrow} RightArrow={RightArrow}>
      {items.map((id) => (
        <Card itemId={id} key={id} title={id} />
      ))}
    </ScrollMenu>
  );
}

function LeftArrow() {
  const visibility = React.useContext<publicApiType>(VisibilityContext);
  const isFirstVisible = visibility.useIsVisible('first', true);
  return (
    <button
      disabled={isFirstVisible}
      onClick={() => visibility.scrollPrev()}
    >

    </button>
  );
}

function RightArrow() {
  const visibility = React.useContext<publicApiType>(VisibilityContext);
  const isLastVisible = visibility.useIsVisible('last', false);
  return (
    <button
      disabled={isLastVisible}
      onClick={() => visibility.scrollNext()}
    >

    </button>
  );
}

function Card({ itemId, title }: { itemId: string; title: string }) {
  const visibility = React.useContext<publicApiType>(VisibilityContext);
  const isVisible = visibility.useIsVisible(itemId);
  return (
    <div className="card" data-visible={isVisible}>
      <div>{title}</div>
      <div>visible: {String(isVisible)}</div>
    </div>
  );
}

The code on the left, running:

item-1
visible: true
item-2
visible: true
item-3
visible: true
item-4
visible: false
item-5
visible: false
item-6
visible: false
item-7
visible: false
item-8
visible: false
item-9
visible: false
item-10
visible: false

itemId обязателен у каждого элемента — на этом работает отслеживание. React key работает как запасной вариант.

styles.css — отдельный импорт; JS-бандл никогда не внедряет CSS.

Ширина элемента задаётся вашим CSS — меню ничего не измеряет.

Читать полный пример «Начало работы»

Или передайте это вашему агент-кодеру

Модели, обученные на старых версиях, всё ещё тянутся к visibleElements, элементам Separator и пропу Arrows — всё это удалено годы назад, — и выдумывают проп autoplay, которого никогда не было. Чтобы это прекратить, пакет поставляет восемь файлов SKILL.md: руководства по конкретным задачам, которые ваш агент загружает по требованию через TanStack Intent, версионируемые с библиотекой, а не с этой страницей.

npx @tanstack/intent@latest install

Запустите один раз в проекте, где пакет уже установлен. Затем ваш агент обнаружит навыки из node_modules/react-horizontal-scrolling-menu/skills/.

  • menu-setupПервое рабочее меню, стрелки, обязательный импорт CSS
  • menu-visibilityЧто на экране и состояние стрелок у краёв
  • menu-scrollingscrollToItem, apiRef, постраничная прокрутка
  • menu-interactionsПеретаскивание, колесо и касание — и их фабрики обработчиков
  • menu-recipesАвтовоспроизведение, бесконечный цикл, подгрузка: рецепты, а не пропсы
  • menu-transitions-rtlТайминги анимации, кастомный easing, справа налево
  • menu-testing-ssrNext.js и RSC, моки Jest, Playwright
  • menu-migrationОбновление кода до v8 и API, которые модели всё ещё выдумывают

Что в коробке

  • Хуки видимости по элементам — useIsVisible(itemId)
  • Хелперы first / last для состояния стрелок
  • scrollToItem · scrollNext · scrollPrev
  • apiRef для управления извне меню
  • Ввод через перетаскивание, колесо, касание и полосу прокрутки
  • Определение динамического добавления/удаления
  • Слоты Header и Footer
  • Хелперы пагинации slidingWindow + getItemsPos
  • Поддержка направления справа налево
  • Кастомные функции переходов
  • Безопасно для SSR — эта страница доказывает
  • TypeScript-first — экспортируется publicApiType
  • Один стабильный API от React 16.8 до 19

Нет в коробке

  • Физика снапа и пружин
  • Полноэкранные слайдеры изображений
  • Лайтбоксы

Это относится к миру слайдеров изображений — Embla и Swiper делают это хорошо. Бесконечный цикл и автовоспроизведение — тоже не пропсы, а рецепты: примерно по шестьдесят строк публичного API каждый, редактируемые вживую в Storybook. Рельс вверху этой страницы — это ровно тот рецепт в действии. Это остаётся меню.

Загружено 347 516 раз за последний месяц примерно 20 000 репозиториев — поддерживается с 2018 года.

788 звёзд на GitHubУпомянуто в React Status #257В продакшене у Our World in Data

Каждый пример редактируется в вашем браузере

Storybook служит песочницей: каждая история поставляется с редактором Monaco, загруженным реальными определениями типов библиотеки. Меняйте код, смотрите на перерисовку — без аккаунта песочницы и локальной настройки.

Создаётся и поддерживается Aleksandr Smyshliaev

Впервые опубликовано в 2018 году, тот же публичный API от React 16.8 до 19. Aleksandr — фронтенд-инженер (React, Next.js, TypeScript), сейчас открыт к контрактной и постоянной работе.