Горизонтальное меню, которое знает, что видно
Меню прокрутки для React на собственной прокрутке браузера — отслеживание видимости по элементам, стрелки, перетаскивание и полный императивный API. 5,7 КБ в gzip.
getVisible() → ['scifi', 'comedy', 'drama', 'horror', 'docs', 'kids']
This is the library, live — drag it. Dimmed tiles are the ones useIsVisible reports as off-screen.
Автовоспроизведение без движка карусели
Пропа autoplay нет — этот рельс является рецептом на публичном API: ряд, склонированный на оба конца, один прыжок scrollLeft на шве и таймер, вызывающий scrollNext(). Он ставится на паузу при наведении, фокусе и на скрытых вкладках, неподвижен при уменьшенной анимации — и его можно перетаскивать, даже назад, через шов.
Это меню, а не карусель
Embla, Swiper и keen-slider заново реализуют прокрутку на JavaScript, чтобы строить слайдеры изображений — точки снапа, пружинная физика, цикл рендера. Эта библиотека не поставляет ничего из этого. Она едет на нативной прокрутке браузера и добавляет то, чего браузер не даёт: точное знание, какие элементы на экране.
Неправильный инструмент для полноэкранного слайдера изображений — там используйте Embla или Swiper. Правильный инструмент для строк категорий, полос вкладок, чип-фильтров и любого ряда вещей, который вашему приложению нужно отслеживать.
Нативная прокрутка
Инерция, полоса прокрутки, касание, колесо и доступность берутся из браузера, а не из физического движка. Ряд прокручивается до того, как ваш JavaScript гидрируется, — каждая демонстрация на этой странице рендерится на сервере.
Отслеживание видимости
IntersectionObserver сообщает, какие элементы на экране. useIsVisible(itemId) подписывает один компонент на один элемент — никакой математики позиций прокрутки, и перерисовываются только затронутые элементы.
Императивность, когда нужно
scrollToItem, scrollNext, scrollPrev, поиск по id или индексу — через контекст внутри меню или apiRef снаружи.
Ваши компоненты, ваш CSS
Стрелки, заголовок, футер и каждый элемент — это компоненты, которые пишете вы. Ширина элемента — ваш CSS. Библиотека поставляет 210 байт стилей раскладки и не мешается.
Быстрый старт
Один файл, без настройки: элементы с itemId, две стрелки, читающие VisibilityContext, и импорт таблицы стилей.
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:
itemId обязателен у каждого элемента — на этом работает отслеживание. React key работает как запасной вариант.
styles.css — отдельный импорт; JS-бандл никогда не внедряет CSS.
Ширина элемента задаётся вашим CSS — меню ничего не измеряет.
Или передайте это вашему агент-кодеру
Модели, обученные на старых версиях, всё ещё тянутся к visibleElements, элементам Separator и пропу Arrows — всё это удалено годы назад, — и выдумывают проп autoplay, которого никогда не было. Чтобы это прекратить, пакет поставляет восемь файлов SKILL.md: руководства по конкретным задачам, которые ваш агент загружает по требованию через TanStack Intent, версионируемые с библиотекой, а не с этой страницей.
Запустите один раз в проекте, где пакет уже установлен. Затем ваш агент обнаружит навыки из node_modules/react-horizontal-scrolling-menu/skills/.
menu-setupПервое рабочее меню, стрелки, обязательный импорт CSSmenu-visibilityЧто на экране и состояние стрелок у краёвmenu-scrollingscrollToItem, apiRef, постраничная прокруткаmenu-interactionsПеретаскивание, колесо и касание — и их фабрики обработчиковmenu-recipesАвтовоспроизведение, бесконечный цикл, подгрузка: рецепты, а не пропсыmenu-transitions-rtlТайминги анимации, кастомный easing, справа налевоmenu-testing-ssrNext.js и RSC, моки Jest, Playwrightmenu-migrationОбновление кода до v8 и API, которые модели всё ещё выдумывают
Рецепты, которые вы реально отправите в прод
Четыре распространённых паттерна, живьём, с ключевыми строками.
Полоса вкладок, которая центрирует активную вкладку
Кликните вкладку: scrollToItem с inline: 'center' приводит её в середину ряда. Тот же вызов обрабатывает start, end и пагинацию.
function Tab({ itemId, label }: { itemId: string; label: string }) {
const api = React.useContext<publicApiType>(VisibilityContext);
const centerOnClick = () => {
const el = api.getItemElementById(itemId);
if (el) api.scrollToItem(el, 'smooth', 'center');
};
return <button onClick={centerOnClick}>{label}</button>;
}Добавить чип, прокрутить к нему
Состояние живёт вне меню; apiRef добирается внутрь. Добавьте фильтр — и ряд последует за ним.
const apiRef = React.useRef<publicApiType>(null);
const lastAdded = React.useRef<string | null>(null);
function addChip(id: string) {
lastAdded.current = id;
setChips((current) => [...current, id]);
}
// After the new chip renders, scroll it into view from outside
// the menu — this is what apiRef is for.
React.useEffect(() => {
const id = lastAdded.current;
if (!id) return;
const el = apiRef.current?.getItemElementById(id);
if (el) apiRef.current?.scrollToItem(el, 'smooth', 'end');
lastAdded.current = null;
}, [chips]);
<ScrollMenu apiRef={apiRef}>…</ScrollMenu>Подгружать, когда виден конец
onUpdate сообщает, когда последний элемент становится видимым, — добавьте следующую страницу прямо там. Ни слушателей прокрутки, ни пиксельных порогов для настройки.
<ScrollMenu
onUpdate={(api) => {
// react in onUpdate, not onScroll — onScroll fires
// before the visibility state settles
if (api.items.last()?.visible) loadMore();
}}
>
{cards}
</ScrollMenu>Справа налево, одним пропом
RTL разворачивает направление контейнера прокрутки; стрелки и логика пагинации следуют.
<ScrollMenu RTL LeftArrow={LeftArrow} RightArrow={RightArrow}>
{items.map((item) => (
<Item itemId={item.id} key={item.id} label={item.label} />
))}
</ScrollMenu>Что в коробке
- Хуки видимости по элементам —
useIsVisible(itemId) - Хелперы
first/lastдля состояния стрелок scrollToItem·scrollNext·scrollPrevapiRefдля управления извне меню- Ввод через перетаскивание, колесо, касание и полосу прокрутки
- Определение динамического добавления/удаления
- Слоты Header и Footer
- Хелперы пагинации
slidingWindow+getItemsPos - Поддержка направления справа налево
- Кастомные функции переходов
- Безопасно для SSR — эта страница доказывает
- TypeScript-first — экспортируется
publicApiType - Один стабильный API от React 16.8 до 19
Нет в коробке
- Физика снапа и пружин
- Полноэкранные слайдеры изображений
- Лайтбоксы
Это относится к миру слайдеров изображений — Embla и Swiper делают это хорошо. Бесконечный цикл и автовоспроизведение — тоже не пропсы, а рецепты: примерно по шестьдесят строк публичного API каждый, редактируемые вживую в Storybook. Рельс вверху этой страницы — это ровно тот рецепт в действии. Это остаётся меню.
Загружено 347 516 раз за последний месяц примерно 20 000 репозиториев — поддерживается с 2018 года.
Каждый пример редактируется в вашем браузере
Storybook служит песочницей: каждая история поставляется с редактором Monaco, загруженным реальными определениями типов библиотеки. Меняйте код, смотрите на перерисовку — без аккаунта песочницы и локальной настройки.
