Горизонтальный ряд в стиле Netflix на React

Ряд постеров, который вы пролистываете на любом стриминговом сайте, — это не карусель, а меню. Ничего не прилипает к позиции, ничего не воспроизводится автоматически: используется нативная инерционная прокрутка, поверх которой наложены стрелки. Именно это и предоставляет react-horizontal-scrolling-menu: ваши карточки, нативная прокрутка и отслеживание видимости каждого элемента, чтобы стрелки знали, когда скрываться.

Перетащите ряд или наведите на него курсор — стрелки проявляются по краям и исчезают по достижении соответствующего конца ряда.

Почему это не задача для карусели

Ряд в стиле Netflix никогда не показывает по одному слайду за раз. Элементы намеренно обрезаны по краям — обрезанный постер служит подсказкой «здесь есть ещё». Карусельные движки борются с этим: они забирают себе слой жестов через JavaScript-трансформации, привязываются к границам слайдов и заново реализуют инерцию, которая у браузеров пользователей уже есть. Для ряда кликабельных карточек всё это — лишние накладные расходы.

Нативная прокрутка бесплатно даёт инерцию, поддержку тач-экрана, трекпада и полосы прокрутки. Единственное, чего она не даёт, — это стрелки поверх контента и знание того, какие карточки видны на экране. Именно эти две вещи добавляет библиотека: через useIsVisible для каждого элемента и состояние стрелок, учитывающее края.

Три детали, которые продают эффект

Масштабирование: ленивые ряды и длинные рейлы

Интерфейсы стриминговых сервисов складывают десятки рядов с сотнями карточек. Поскольку элементы — это обычный DOM в нативном контейнере прокрутки, при прокрутке ничего не перерисовывается — пример на производительность обрабатывает 300 элементов без виртуализации. Видимость по каждому элементу также бесплатно даёт ленивую загрузку изображений: рендерите заглушку, пока useIsVisible не сообщит, что карточка на экране.

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

Паттерн в минимальном виде

Наложите стрелки поверх ряда с нативной прокруткой — демо выше это и есть данная структура плюс стилизация. Готовый к использованию код с перетаскиванием и затуханием краёв доступен ниже в виде компонента shadcn.

MediaRow.tsx
function MediaRow({ children }: { children: React.ReactNode }) {
  return (
    <div className="group relative">
      <ScrollMenu Header={<OverlayArrows />} wrapperClassName="edge-fade">
        {children}
      </ScrollMenu>
    </div>
  );
}

// Rendered as Header, so it lives inside the menu's VisibilityContext
// and can sit absolutely positioned over the row ends.
function OverlayArrows() {
  const api = React.useContext<publicApiType>(VisibilityContext);
  const atStart = api.useLeftArrowVisible();
  const atEnd = api.useRightArrowVisible();
  return (
    <>
      <button
        className="overlay-arrow left-0"
        hidden={atStart}
        onClick={() => api.scrollPrev()}
      >

      </button>
      <button
        className="overlay-arrow right-0"
        hidden={atEnd}
        onClick={() => api.scrollNext()}
      >

      </button>
    </>
  );
}

/* .edge-fade — the Netflix edge, one CSS line:
   mask-image: linear-gradient(to right,
     transparent, black 40px, black calc(100% - 40px), transparent); */

Или установите как компонент shadcn

Элемент реестра media-row — это именно этот паттерн: стрелки при наведении, градиентное затухание краёв, перетаскивание для прокрутки — в виде компонента на Tailwind в вашей components/ui/, который можно редактировать:

shadcn
npx shadcn@latest add https://react-horizontal-scrolling-menu.dev/r/media-row.json

Похожие примеры

Все примеры (21)