React で作る Netflix 風の横スクロール行
どのストリーミングサイトでもスクロールするあのポスター行は、カルーセルではなくメニューです。スナップもオートプレイもせず、矢印を重ねたままネイティブの慣性スクロールに乗るだけ。それこそが react-horizontal-scrolling-menu が提供するものです — あなたのカードとネイティブスクロール、そして矢印がいつ隠れるべきかを知るためのアイテムごとの可視性判定。
ドラッグするか、行にホバーしてみてください — 矢印が端にフェードインし、行のその端に到達すると消えます。
なぜこれはカルーセルの仕事ではないのか
Netflix の行は一度に 1 枚のスライドだけを見せることは決してありません。アイテムが端で意図的に一部だけ表示されているのは、「まだ続きがある」ことを伝えるアフォーダンスです。カルーセルエンジンはこれに逆らいます — JavaScript のトランスフォームでジェスチャー層を独占し、スライドの境界にスナップし、ユーザーのブラウザーがすでに持っている慣性を再実装します。クリック可能なカードの行では、そのすべてが余計なオーバーヘッドです。
ネイティブスクロールなら慣性、タッチ、トラックパッド、スクロールバーがすべて無料で手に入ります。得られないのはオーバーレイの矢印と、どのカードが画面内にあるかを知ることの 2 点だけ — このライブラリが加えるのはまさにその 2 点で、アイテムごとの useIsVisible とエッジ対応の矢印状態です。
その効果を成立させる 3 つの細部
- 矢印はコンテンツに重ねて表示し、横に並べません。行の両端に絶対配置でレンダリングし(上のデモではメニューのコンテキスト内に留めるため
Headerを通して渡しています)、ホバー時に表示し、useLeftArrowVisible/useRightArrowVisibleがその端に到達したと報告したら、それぞれを非表示にします。 - 端はフェードします。 スクロールコンテナへの
mask-imageグラデーション 1 行の CSS が、カルーセルプラグインがこのために用意する「peek」ロジックを置き換えます。 - ドラッグはクリックを発火させてはいけません。 ポスター上で終わるマウスドラッグは、それを開いてはいけません。ドラッグスクロールのレシピ がドラッグ状態を追跡し、そのクリックだけを飲み込みます。
スケールさせる:遅延読み込みの行と長いレール
ストリーミング UI は数百枚のカードを持つ行を何十も積み重ねます。アイテムはネイティブスクロールコンテナ内の普通の DOM なので、スクロールで再レンダリングは一切起きません — パフォーマンスの例 は仮想化なしで 300 個のアイテムを動かしています。アイテムごとの可視性は画像の遅延読み込みも無料で提供します:useIsVisible がカードが画面内にあると報告するまでプレースホルダーをレンダリングするだけです。
行が端で折り返すべき場合、それはスライドのセマンティクスが本当に役立つ数少ない場面です — カルーセルエンジンに頼る前に、無限ループのレシピ にある約 60 行のユーザーランド実装を見てください。
パターン、最小構成
ネイティブスクロールの行に矢印を重ねる — 上のデモはこの構造にスタイリングを加えたものです。ドラッグとエッジフェードを備えた、そのまま使える完全なソースは下の shadcn コンポーネントとして提供されています。
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 レジストリアイテムは、まさにこのパターン — ホバー矢印、グラデーションのエッジフェード、ドラッグスクロール — を、あなたの components/ui/ に置く Tailwind スタイルのコンポーネントとして提供し、自由に編集できます:
npx shadcn@latest add https://react-horizontal-scrolling-menu.dev/r/media-row.json