Menu ngang mà biết cái gì đang hiển thị
Một menu cuộn React được xây trên chính cuộn của trình duyệt — theo dõi khả năng hiển thị theo từng mục, mũi tên, kéo và một API mệnh lệnh đầy đủ. 5.7 kB 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.
Tự phát, không cần engine carousel
Không có prop autoplay — ray này là một recipe trên API công khai: hàng được nhân bản ra hai đầu, một cú nhảy scrollLeft tại đường nối và một bộ hẹn giờ gọi scrollNext(). Nó tạm dừng khi hover, focus và trên các tab bị ẩn, đứng yên dưới chế độ giảm chuyển động — và bạn có thể kéo nó, thậm chí ngược lại, qua đường nối.
Một menu, không phải carousel
Embla, Swiper và keen-slider triển khai lại việc cuộn bằng JavaScript để xây slider ảnh — điểm snap, vật lý lò xo, một vòng lặp render. Thư viện này không gửi kèm bất cứ thứ gì trong số đó. Nó dùng cuộn gốc của trình duyệt và thêm thứ duy nhất trình duyệt không cho bạn: biết chính xác mục nào đang trên màn hình.
Sai công cụ cho một slider ảnh toàn màn hình — hãy dùng Embla hoặc Swiper ở đó. Đúng công cụ cho các hàng danh mục, dải tab, bộ lọc chip và bất kỳ hàng nội dung nào mà ứng dụng của bạn cần xử lý.
Cuộn gốc
Quán tính, thanh cuộn, cảm ứng, con lăn và khả năng tiếp cận đến từ trình duyệt, không phải một engine vật lý. Hàng cuộn trước khi JavaScript của bạn hydrate — mọi bản demo trên trang này được render trên server.
Theo dõi khả năng hiển thị
IntersectionObserver báo cáo mục nào đang trên màn hình. useIsVisible(itemId) đăng ký một component với một mục — không tính toán vị trí cuộn, và chỉ các mục bị ảnh hưởng mới render lại.
Mệnh lệnh khi bạn cần
scrollToItem, scrollNext, scrollPrev, tra cứu theo id hoặc index — qua context trong menu, hoặc apiRef từ bên ngoài.
Component của bạn, CSS của bạn
Mũi tên, header, footer và mọi mục là component bạn viết. Chiều rộng mục là CSS của bạn. Thư viện gửi kèm 210 byte style bố cục và tránh ra khỏi đường.
Bắt đầu nhanh
Một file, không cấu hình: các mục với itemId, hai mũi tên đọc VisibilityContext và import stylesheet.
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 bắt buộc trên mọi mục — đó là cách theo dõi hoạt động. key của React hoạt động như một phương án dự phòng.
styles.css là một import riêng; bundle JS không bao giờ tiêm CSS.
Chiều rộng mục đến từ CSS của riêng bạn — menu không đo lường gì cả.
Hoặc giao cho coding agent của bạn
Các mô hình được huấn luyện trên các phiên bản cũ vẫn tìm đến visibleElements, các mục Separator và một prop Arrows — tất cả đã bị xóa nhiều năm trước — và bịa ra một prop autoplay chưa từng tồn tại. Để ngăn điều đó, gói này gửi kèm tám file SKILL.md: hướng dẫn theo nhiệm vụ mà agent của bạn tải theo yêu cầu qua TanStack Intent, được lập phiên bản cùng thư viện thay vì cùng trang này.
Chạy một lần trong dự án đã cài gói. Agent của bạn sau đó khám phá các kỹ năng từ node_modules/react-horizontal-scrolling-menu/skills/.
menu-setupMenu đầu tiên hoạt động, mũi tên, import CSS bắt buộcmenu-visibilityCái gì đang trên màn hình và trạng thái mũi tên ở hai đầumenu-scrollingscrollToItem, apiRef, phân trang từng trangmenu-interactionsKéo, con lăn và cảm ứng — và các factory xử lý của chúngmenu-recipesTự phát, vòng lặp vô hạn, tải thêm: recipe, không phải propsmenu-transitions-rtlThời gian hoạt ảnh, easing tùy chỉnh, phải sang tráimenu-testing-ssrNext.js và RSC, mock Jest, Playwrightmenu-migrationNâng cấp code trước v8 và các API mà mô hình vẫn bịa ra
Đọc các kỹ năng trên GitHub llms.txt — cùng các thông tin, cô đọng
Những recipe bạn sẽ thực sự đưa lên
Bốn pattern phổ biến, trực tiếp, với những dòng quan trọng.
Một dải tab căn giữa tab đang hoạt động
Bấm một tab: scrollToItem với inline: 'center' đưa nó ra giữa hàng. Cùng lệnh đó xử lý start, end và phân trang.
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>;
}Thêm một chip, cuộn đến nó
Trạng thái nằm ngoài menu; apiRef với vào trong. Thêm một bộ lọc và hàng theo nó.
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>Tải thêm khi thấy điểm cuối
onUpdate cho bạn biết khi mục cuối trở nên hiển thị — thêm trang kế tiếp ngay tại đó. Không có listener cuộn, không có ngưỡng pixel phải chỉnh.
<ScrollMenu
onUpdate={(api) => {
// react in onUpdate, not onScroll — onScroll fires
// before the visibility state settles
if (api.items.last()?.visible) loadMore();
}}
>
{cards}
</ScrollMenu>Phải sang trái, một prop
RTL lật hướng của container cuộn; mũi tên và logic phân trang theo sau.
<ScrollMenu RTL LeftArrow={LeftArrow} RightArrow={RightArrow}>
{items.map((item) => (
<Item itemId={item.id} key={item.id} label={item.label} />
))}
</ScrollMenu>Có gì trong hộp
- Hook khả năng hiển thị theo từng mục —
useIsVisible(itemId) - Helper
first/lastcho trạng thái mũi tên scrollToItem·scrollNext·scrollPrevapiRefđể điều khiển từ ngoài menu- Đầu vào kéo, con lăn, cảm ứng và thanh cuộn
- Phát hiện thêm/xóa động
- Slot Header và Footer
- Helper phân trang
slidingWindow+getItemsPos - Hỗ trợ phải sang trái
- Hàm chuyển tiếp tùy chỉnh
- An toàn với SSR — trang này chứng minh
- TypeScript-first —
publicApiTypeđược export - Một API ổn định qua React 16.8 – 19
Không có trong hộp
- Vật lý snap và lò xo
- Slider ảnh toàn màn hình
- Lightbox
Đó thuộc về thế giới slider ảnh — Embla và Swiper làm tốt. Vòng lặp vô hạn và tự phát cũng không phải props — chúng là recipe: khoảng sáu mươi dòng của API công khai mỗi cái, có thể chỉnh sửa trực tiếp trong Storybook. Ray gần đầu trang này chính là recipe đó đang chạy. Cái này vẫn là một menu.
Được tải 347,516 lần tháng trước bởi khoảng 20,000 kho lưu trữ — được bảo trì từ 2018.
Mọi ví dụ đều có thể chỉnh sửa, trong trình duyệt của bạn
Storybook kiêm vai trò sân chơi: mỗi story gửi kèm một trình soạn thảo Monaco được nạp các định nghĩa kiểu thực của thư viện. Thay đổi code, xem nó render lại — không cần tài khoản sandbox, không cần thiết lập cục bộ.
