Build a working react-horizontal-scrolling-menu: install, the mandatory 'react-horizontal-scrolling-menu/dist/styles.css' import, ScrollMenu with a unique itemId per child, arrow components via VisibilityContext with useLeftArrowVisible/useRightArrowVisible, Header/Footer slots, and CSS customization (fixed item width, spacing via gap on the scroll container, hiding the scrollbar). Load when creating a category row, tab strip, chip filter, or any horizontal scrolling list, or when a menu renders vertically, arrows do nothing, or item visibility is not tracked.
Build a working react-horizontal-scrolling-menu: install, the mandatory 'react-horizontal-scrolling-menu/dist/styles.css' import, ScrollMenu with a unique itemId per child, arrow components via VisibilityContext with useLeftArrowVisible/useRightArrowVisible, Header/Footer slots, and CSS customization (fixed item width, spacing via gap on the scroll container, hiding the scrollbar). Load when creating a category row, tab strip, chip filter, or any horizontal scrolling list, or when a menu renders vertically, arrows do nothing, or item visibility is not tracked.
Three contracts this relies on, none of which produce an error when violated:
Every direct child of ScrollMenu needs a unique itemId prop (the React
key is the fallback). Ids are String()-coerced.
styles.css is a separate import — the JS bundle never injects CSS.
Item width comes from your CSS, in fixed units. The menu measures nothing.
The arrow hooks are the canonical edge detection: useLeftArrowVisible() /
useRightArrowVisible() return true when the first/last item is visible —
use the return value directly as the arrow's disabled state, as above.
Despite the "Visible" in the name, this is a disabled-latch: it only updates
while the menu itself is on screen, so arrows do not flicker when the page
scrolls vertically past the menu (src/createApi.ts:65-89).
Core Patterns
Header and Footer slots
Header and Footer render full-width above/below the arrows+items row.
Like the arrows, they are rendered as bare elements with no props — read
state from VisibilityContext:
Both a component reference (Footer={EndIndicator}) and an element
(Header={<h2>Categories</h2>}) are accepted
(src/helpers.tsx:88-99 getElementOrConstructor). Read reactive state
through the context hooks (useIsVisible, the arrow hooks): a plain render
of api.items.getVisible() never updates, because visibility changes mutate
the ItemsMap without re-rendering the menu.
The itemId contract — self-check uniqueness
itemId is read from each direct child's props (React key as fallback) and
String()-coerced (src/helpers.tsx:101-105). The internal ItemsMap is a
Map keyed by that string: duplicates overwrite each other, last one wins,
and the library never warns — visibility and scrollToItem then silently
target the wrong element. Pick a field that is unique per rendered item and
verify it yourself:
Each child is wrapped in a div.react-horizontal-scrolling-menu--item with
data-key={itemId} and data-index attributes (src/components/Item) — handy
for e2e selectors and document.querySelector.
Styling
The menu ships structural CSS only; all visual customization is your CSS
against the shipped class names (src/constants.ts):
The itemClassName, scrollContainerClassName and wrapperClassName props
append your own class next to the shipped one (src/index.tsx:117-128) — use
them to scope styles per menu instance.
The three customizations every real menu needs:
/* menu.css *//* 1. Fixed item width — always px/rem, never percent */.card {
width: 160px;
}
/* 2. Spacing between items: gap on the scroll container
(or margin on your item component). There is NO gap/separator prop —
separator elements were removed in v7. */.react-horizontal-scrolling-menu--scroll-container {
gap: 8px;
}
/* 3. Hide the native scrollbar */.react-horizontal-scrolling-menu--scroll-container {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* old Edge/IE */
}
.react-horizontal-scrolling-menu--scroll-container::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
Every direct child needs a unique itemId prop (React key is the
fallback); without it items register under an empty id, colliding in the
ItemsMap, and arrows/visibility silently break — there is no runtime warning.
HIGH: renaming itemId to itemID to silence the React DOM warning
Wrong:
// "fixing" the console warning:
<Card itemID={id} key={id} />
Correct:
// keep itemId; don't spread it onto the DOM node inside CardfunctionCard({ itemId, ...rest }: { itemId: string; title: string }) {
return<div>{rest.title}</div>;
}
When an item spreads props onto a DOM element React warns "Invalid DOM
property itemId. Did you mean itemID?" — the warning is expected and
harmless; renaming the prop breaks tracking entirely.
ItemsMap extends Map keyed by String(itemId): duplicates overwrite each
other (last wins) with zero warnings, and visibility and scrollToItem then
target the wrong element — self-check uniqueness (see Core Patterns) because
the library never will.
LeftArrow/RightArrow/Header/Footer are rendered as <Elem /> with no
props (getElementOrConstructor); arrows must read state from
React.useContext(VisibilityContext).
Separator elements were removed in v7; spacing is plain CSS — gap on the
scroll container (via scrollContainerClassName or the shipped class) or
margin on your item component.
Source: maintainer interview; CHANGELOG v7.0.0 (#274) — also covered in skills/menu-migration/SKILL.md
Tensions
HIGH Tension: trivial quick start vs total silence on misuse
The happy path is a few lines, but the library contains zero throws or
warnings — every contract violation (missing CSS import, missing/duplicate
itemId, passing a string id to scrollToItem) fails silently. Shipping the
quick start without self-checking the contracts produces a menu that renders
but does not work, with no error to debug from. Before declaring setup done,
verify: the dist/styles.css import exists, every child has a unique
string itemId, and arrows use the context hooks. For the visibility and
scrolling halves of this tension see skills/menu-visibility/SKILL.md and
skills/menu-scrolling/SKILL.md.
See also
skills/menu-visibility/SKILL.md — canonical arrows are visibility-driven;
setup code quality depends on using the reactive hooks
(useLeftArrowVisible/useRightArrowVisible/useIsVisible), not the
frozen isFirstItemVisible/isLastItemVisible snapshots.