Menu Scrolling logo

Menu Scrolling

Community
asmyshlyaev177
menu-scrolling

Imperative scrolling for react-horizontal-scrolling-menu: scrollToItem(getItemById(id), behavior, inline, block), scrollNext/scrollPrev, apiRef for controlling the menu from outside (fire methods, never read data), getItemElementById/getItemElementByIndex for just-added items, and page-at-a-time navigation with slidingWindow + getItemsPos. Load when scrolling to an item on mount or selection, controlling the menu from outside, scrolling after adding items dynamically, or when scrollToItem silently does nothing or the whole page jumps to the menu.

Overview

Publisherasmyshlyaev177
Repositoryreact-horizontal-scrolling-menu
Skill namemenu-scrolling
Stars
790
Forks
101
Bundled files
1
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 1 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by asmyshlyaev177 on GitHub. Read the source before you install it.

Installation

Install the Menu Scrolling AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/tree/master/skills/menu-scrolling
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu.git /tmp/react-horizontal-scrolling-menu
mkdir -p .claude/skills
cp -r /tmp/react-horizontal-scrolling-menu/skills/menu-scrolling .claude/skills/menu-scrolling
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Menu Scrolling in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Menu Scrolling on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Menu Scrolling is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Imperative Scrolling

Setup

Click an item to center it. The api object comes from VisibilityContext inside the menu, from the apiRef prop outside it, and as the first argument of every callback prop (onInit, onUpdate, onWheel, onScroll).

tsx
import React from 'react';
import {
  ScrollMenu,
  VisibilityContext,
  type publicApiType,
} from 'react-horizontal-scrolling-menu';
import 'react-horizontal-scrolling-menu/dist/styles.css';

const ids = ['item-0', 'item-1', 'item-2', 'item-3', 'item-4', 'item-5'];

function Card({ itemId, title }: { itemId: string; title: string }) {
  const api = React.useContext<publicApiType>(VisibilityContext);
  return (
    <div
      role="button"
      tabIndex={0}
      style={{ width: '160px' }}
      onClick={() =>
        api.scrollToItem(api.getItemById(itemId), 'smooth', 'center')
      }
    >
      {title}
    </div>
  );
}

export function Menu() {
  return (
    <ScrollMenu>
      {ids.map((id) => (
        <Card itemId={id} key={id} title={id} />
      ))}
    </ScrollMenu>
  );
}

The blessed form is api.scrollToItem(api.getItemById(id), 'smooth', 'center') — a bare string id is a silent no-op (see Common Mistakes). getItemElementById(id) / getItemElementByIndex(index) are the advanced form: they query the DOM directly by data-key / data-index, which makes them stale-proof for items added in the current render.

Defaults (src/helpers.tsx:60,68-69; src/createApi.ts:131-132,152-153):

Methodbehaviorinlineblock
scrollToItem'smooth''end''nearest'
scrollPrev'smooth''end''nearest'
scrollNext'smooth''start''nearest'

behavior accepts 'auto' | 'instant' | 'smooth' and falls back to the transitionBehavior prop when one is set. The optional trailing { duration, boundary } argument only takes effect with noPolyfill={false} — the default native scroll ignores it.

Core Patterns

Control the menu from outside with apiRef

Pass a ref to ScrollMenu; the full context value is assigned to it after mount. Use it to fire methods only — data read from it goes stale.

tsx
import React from 'react';
import {
  ScrollMenu,
  type publicApiType,
} from 'react-horizontal-scrolling-menu';
import 'react-horizontal-scrolling-menu/dist/styles.css';

const ids = ['item-0', 'item-1', 'item-2', 'item-3', 'item-4', 'item-5'];

function Card({ itemId, title }: { itemId: string; title: string }) {
  return <div style={{ width: '160px' }}>{title}</div>;
}

export function PageWithExternalControls() {
  const apiRef = React.useRef<publicApiType | null>(null);

  const centerItem = (id: string) => {
    const api = apiRef.current;
    if (api) api.scrollToItem(api.getItemById(id), 'smooth', 'center');
  };

  return (
    <div>
      <button onClick={() => apiRef.current?.scrollPrev()}>Prev</button>
      <button onClick={() => centerItem('item-3')}>Center item-3</button>
      <button onClick={() => apiRef.current?.scrollNext()}>Next</button>
      <ScrollMenu apiRef={apiRef}>
        {ids.map((id) => (
          <Card itemId={id} key={id} title={id} />
        ))}
      </ScrollMenu>
    </div>
  );
}

Scroll to an item on mount

onInit fires once the menu has rendered and measured its items, so the api is safe to use right away — no timers. Use 'auto' (instant) so the initial position does not animate.

tsx
import React from 'react';
import {
  ScrollMenu,
  type publicApiType,
} from 'react-horizontal-scrolling-menu';
import 'react-horizontal-scrolling-menu/dist/styles.css';

const ids = ['item-0', 'item-1', 'item-2', 'item-3', 'item-4', 'item-5'];

function Card({ itemId, title }: { itemId: string; title: string }) {
  return <div style={{ width: '160px' }}>{title}</div>;
}

export function MenuStartingAtItemFive() {
  const scrollToItemOnInit = (api: publicApiType) => {
    const el = api.getItemElementById('item-5');
    if (el) api.scrollToItem(el, 'auto', 'start');
  };

  return (
    <ScrollMenu onInit={scrollToItemOnInit}>
      {ids.map((id) => (
        <Card itemId={id} key={id} title={id} />
      ))}
    </ScrollMenu>
  );
}

Page-at-a-time navigation with slidingWindow and getItemsPos

scrollNext/scrollPrev already move one viewport-group. Use slidingWindow + getItemsPos when you need to control which item of the target group lands where — e.g. centering the next page:

tsx
import {
  getItemsPos,
  slidingWindow,
  type publicApiType,
} from 'react-horizontal-scrolling-menu';

function scrollOnePage(api: publicApiType, direction: 'prev' | 'next') {
  const visible = api.items.getVisible().map(([id]) => id);
  if (!visible.length) return;
  const group = slidingWindow(api.items.toItems(), visible)[direction]();
  const target = getItemsPos(group).center;
  api.scrollToItem(api.getItemById(target), 'smooth', 'center');
}

slidingWindow(allIds, visibleIds) returns { prev(), next() } — each an id array the size of the visible set, clamped at the row edges. getItemsPos(group) returns { first, center, last } ids of that group.

Common Mistakes

CRITICAL Passing an itemId string to scrollToItem

Wrong:

tsx
api.scrollToItem('item-3', 'smooth', 'center'); // nothing scrolls

Correct:

tsx
api.scrollToItem(api.getItemById('item-3'), 'smooth', 'center');

The JSDoc example shows a bare 'itemId', but scrollToItem unwraps target?.entry?.target — a string has no .entry and no .scrollIntoView, so the call is a silent no-op (the TS type ItemOrElement correctly rejects strings; plain JS gets no error at all).

Source: src/createApi.ts:307-313 (JSDoc) vs src/helpers.tsx:59-64; https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/issues/157

HIGH Reading data values from apiRef during render

Wrong:

tsx
const atEnd = apiRef.current?.isLastItemVisible; // in render: never re-renders

Correct:

tsx
const api = apiRef.current;
if (api) api.scrollToItem(api.getItemById('item-3'), 'smooth', 'center');

apiRef is a mutable object React cannot re-render on — values read from it during render go stale. Inside event handlers and timers the visibility getters read the live ItemsMap and are fine (on <= 8.3.1 they were frozen even there — read api.items.last()?.visible instead). Reactive state in components comes from the context hooks.

Source: README.md apiRef section; https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/issues/167

HIGH getItemById right after adding an item returns undefined

Wrong:

tsx
setItems((prev) => [...prev, newItem]);
const api = apiRef.current;
if (api) api.scrollToItem(api.getItemById(newItem.id)); // undefined target — no-op

Correct:

tsx
// after the state update commits (effect or callback):
const api = apiRef.current;
const el = api?.getItemElementById(newItem.id);
if (api && el) api.scrollToItem(el, 'smooth', 'end');

The ItemsMap lags children by one render, so the map does not know a just-appended item yet; getItemElementById/getItemElementByIndex query the DOM by data-key/data-index and are stale-proof for this case.

Source: https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/issues/167; discussion #295; stories/AddItemAndScrollToIt

HIGH Scroll methods drag the whole page to the menu

Wrong:

tsx
setInterval(() => apiRef.current?.scrollNext(), 3000); // page keeps jumping to the menu

Correct:

tsx
setInterval(() => {
  if (apiRef.current?.menuVisible.current) apiRef.current.scrollNext();
}, 3000);

Scrolling is scrollIntoView-based, so scroll methods called while the menu is off screen scroll ancestor containers too (the page jumps vertically to the menu) — gate every programmatic scroll on menuVisible.current.

Source: https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/issues/276 (#277, #174, #230)

MEDIUM Numeric itemIds compared as numbers

Wrong:

tsx
<Card itemId={idx} key={idx} title={String(idx)} />;
// later:
api.getItemById(idx + 1);

Correct:

tsx
<Card itemId={String(idx)} key={idx} title={String(idx)} />;
// later:
api.getItemById(String(idx + 1));

itemId is String()-coerced everywhere internally, so getItemById(5) looks up "5" — mixing number and string ids causes missed lookups.

Source: CHANGELOG v3.1.1 (#207); src/helpers.tsx:102

MEDIUM Calling apiRef.current methods before mount

Wrong:

tsx
const apiRef = React.useRef<publicApiType | null>(null);
apiRef.current.scrollNext(); // during render — crash or no-op

Correct:

tsx
const apiRef = React.useRef<publicApiType | null>(null);
React.useEffect(() => {
  apiRef.current?.scrollNext();
}, []);

apiRef is populated in a useEffect after mount (src/index.tsx:281-287); before that current is null (or an empty object internally) — call methods only from effects or event handlers, with optional chaining.

Source: src/index.tsx:148,281-287

MEDIUM Reimplementing page math instead of slidingWindow/getItemsPos

Wrong:

tsx
const all = api.items.toItems();
const firstVisible = api.items.getVisible()[0]?.[0];
const next = all[all.indexOf(firstVisible) + 3]; // breaks at edges, wrong in RTL

Correct:

tsx
const visible = api.items.getVisible().map(([id]) => id);
const next = slidingWindow(api.items.toItems(), visible).next();
api.scrollToItem(api.getItemById(getItemsPos(next).center), 'smooth', 'center');

Page-at-a-time and centering math is shipped — slidingWindow().prev()/.next() plus getItemsPos() handle row edges and RTL; hand-rolled index math misses both.

Source: README.md Other helpers; stories/OneItemScroll

Tensions

HIGH Tension: Trivial quick start vs total silence on misuse

The library contains zero throws or warnings — every contract violation (missing styles.css, missing/duplicate itemId, string to scrollToItem) fails silently with no error to debug from. Self-check the contracts before shipping. See skills/menu-setup/SKILL.md and skills/menu-visibility/SKILL.md.

HIGH Tension: Imperative convenience vs reactive truth

The api object mixes live methods, reactive hooks, non-reactive getters (isFirstItemVisible/isLastItemVisible) and mutable stores (items, apiRef). Fire methods imperatively; in components rendered under ScrollMenu read state through the hooks — render reads of the getters never update. See skills/menu-visibility/SKILL.md.

HIGH Tension: Native scroll correctness vs animation control

noPolyfill defaults to true, so the per-call { duration, boundary } ScrollOptions and the transition props are silently discarded; noPolyfill={false} restores animation control but re-imports polyfill edge bugs (RTL, page-level scrolling). See skills/menu-transitions-rtl/SKILL.md.

See also

  • skills/menu-visibility/SKILL.md — paging math consumes the visible set (items.getVisible()), and scroll gating uses menuVisible
  • skills/menu-recipes/SKILL.md — autoplay, infinite loop, load-more and center-on-click are recipes built from these scroll methods
  • skills/menu-transitions-rtl/SKILL.md — transition props and ScrollOptions modify how the scroll methods animate

References

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Menu Scrolling AI skill do?

Imperative scrolling for react-horizontal-scrolling-menu: scrollToItem(getItemById(id), behavior, inline, block), scrollNext/scrollPrev, apiRef for controlling the menu from outside (fire methods, never read data), getItemElementById/getItemElementByIndex for just-added items, and page-at-a-time navigation with slidingWindow + getItemsPos. Load when scrolling to an item on mount or selection, controlling the menu from outside, scrolling after adding items dynamically, or when scrollToItem silently does nothing or the whole page jumps to the menu.

Why use Menu Scrolling on TypingMind?

Because you install it once and use it with any model. Menu Scrolling is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Menu Scrolling in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/tree/master/skills/menu-scrolling. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Menu Scrolling?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Menu Scrolling?

As many as you like. As long as a model supports skills, you can use Menu Scrolling with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Menu Scrolling AI skill free?

Yes. It is published on GitHub by asmyshlyaev177 under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇