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 hydrate 之前就能滚动——本页每个演示都是服务端渲染的。
可见性追踪
IntersectionObserver 报告哪些项目在屏幕上。useIsVisible(itemId) 让一个组件订阅一个项目——无需滚动位置计算,并且只有受影响的那些项目会重新渲染。
需要时命令式
scrollToItem、scrollNext、scrollPrev、按 id 或索引查找——通过菜单内部的 context,或来自外部的 apiRef。
你的组件,你的 CSS
箭头、header、footer 与每个项目都是你编写的组件。项目宽度是你的 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第一个可用的菜单、箭头、必需的 CSS 导入menu-visibility屏幕上有什么,以及两端的箭头状态menu-scrollingscrollToItem、apiRef、一次一页的分页menu-interactions拖拽、滚轮与触摸——以及它们的事件处理工厂menu-recipes自动播放、无限循环、加载更多:是配方,不是属性menu-transitions-rtl动画时长、自定义缓动、从右到左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>盒子里有什么
- 逐项可见性 hook——
useIsVisible(itemId) - 用于箭头状态的
first/last辅助函数 scrollToItem·scrollNext·scrollPrev- 用于从菜单外部控制的
apiRef - 拖拽、滚轮、触摸与滚动条输入
- 动态增删检测
- Header 与 Footer 插槽
slidingWindow+getItemsPos分页辅助函数- 从右到左支持
- 自定义过渡函数
- SSR 安全——本页就是证明
- TypeScript 优先——导出
publicApiType - 在 React 16.8 – 19 之间保持一套稳定的 API
上个月被约 20,000 个仓库下载了 347,516 次——自 2018 年维护至今。
每个示例都可以在浏览器里编辑
这个 Storybook 同时也是一个沙盒:每个 story 都附带一个加载了库的真实类型定义的 Monaco 编辑器。改代码,看它重新渲染——无需沙盒账号,也无需本地搭建。
