Menu Recipes logo

Menu Recipes

Community
asmyshlyaev177
menu-recipes

Recipes composed on the react-horizontal-scrolling-menu public API — NOT props: autoplay (setInterval + scrollNext gated on menuVisible), infinite loop/carousel (clone head/tail + scrollLeft teleport at seams), center on click (scrollToItem 'center'), save/restore scroll position (onUpdate, onInit, scrollContainer), load-more with a loader item (items.last()?.visible in onUpdate), one item per scroll (getPrevElement/getNextElement), snap to a card (CSS scroll-snap on scrollContainerClassName/itemClassName, current card from onScroll, drag with snap switched off), hiding arrows, multiple menus, tab switching. Load when asked for autoplay, looping, snapping, a testimonial/card carousel, infinite scroll, or any feature missing from the props table — inventing an autoplay/loop/snap prop generates dead code.

Overview

Publisherasmyshlyaev177
Repositoryreact-horizontal-scrolling-menu
Skill namemenu-recipes
Stars
790
Forks
101
Bundled files
3
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.

  • 3 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 Recipes 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-recipes
  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-recipes .claude/skills/menu-recipes
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Menu Recipes 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 Recipes 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 Recipes 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.

react-horizontal-scrolling-menu — Recipes

No autoplay, loop, or snap props exist, by design (README "What it does — and doesn't"). Each feature below is a ~60-line recipe on the public API; snapping to a card is CSS scroll-snap, not physics. Generate the recipe, never a prop.

Every recipe is also a live-editable Storybook story — URLs and source paths in references/stories.md.

Setup

Shared base every pattern below builds on (scrollbar hiding and item spacing are plain CSS — see skills/menu-setup/SKILL.md):

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 = Array.from({ length: 10 }, (_, i) => `item-${i}`);

function LeftArrow() {
  const api = React.useContext<publicApiType>(VisibilityContext);
  const disabled = api.useLeftArrowVisible();
  return (
    <button disabled={disabled} onClick={() => api.scrollPrev()}>
      Prev
    </button>
  );
}
function RightArrow() {
  const api = React.useContext<publicApiType>(VisibilityContext);
  const disabled = api.useRightArrowVisible();
  return (
    <button disabled={disabled} onClick={() => api.scrollNext()}>
      Next
    </button>
  );
}
function Card({ itemId, title }: { itemId: string; title: string }) {
  const api = React.useContext<publicApiType>(VisibilityContext);
  const visible = api.useIsVisible(itemId, true);
  return <div style={{ width: 160, opacity: visible ? 1 : 0.5 }}>{title}</div>;
}
// Children below are written as ids.map(renderCard) to keep recipes short.
const renderCard = (id: string) => <Card itemId={id} key={id} title={id} />;

Core Patterns

Autoplay: interval calling scrollNext, gated on menu visibility

tsx
export function AutoplayMenu({ interval = 3000 }: { interval?: number }) {
  const apiRef = React.useRef<publicApiType | null>(null);
  const [paused, setPaused] = React.useState(false);
  React.useEffect(() => {
    if (paused) return;
    const id = window.setInterval(() => {
      const api = apiRef.current;
      // Off-screen scrollNext drags the page; a hidden tab freezes IO.
      if (!api?.menuVisible.current || document.visibilityState !== 'visible')
        return;
      api.scrollNext();
    }, interval);
    return () => window.clearInterval(id);
  }, [paused, interval]);
  return (
    <div
      onMouseEnter={() => setPaused(true)}
      onMouseLeave={() => setPaused(false)}
    >
      <ScrollMenu LeftArrow={LeftArrow} RightArrow={RightArrow} apiRef={apiRef}>
        {ids.map(renderCard)}
      </ScrollMenu>
    </div>
  );
}

The full story also pauses on touch/focus, stays off under prefers-reduced-motion (WCAG 2.2.2), and layers this timer on the infinite-loop hook below so playback never hits the end of the row.

Infinite loop: clone head/tail, teleport scrollLeft at the seams

Render [tailClones, ...items, headClones]; when scrolling settles inside a clone zone, shift scrollLeft by one loop length — clone zones are pixel-identical, so the jump is invisible.

tsx
export function LoopMenu() {
  const loop = useInfiniteLoop(ids); // references/infinite-loop.md
  return (
    <ScrollMenu {...loop.menuProps} LeftArrow={LoopPrev} RightArrow={LoopNext}>
      {loop.slides.map(({ itemId, realId }) => (
        // itemId must stay unique (clone suffix); display/selection use realId
        <Card itemId={itemId} key={itemId} title={realId} />
      ))}
    </ScrollMenu>
  );
}

Four parts of the hook are load-bearing, and each one is a bug if dropped:

  • clone zones at least a viewport wide per side (CLONES_PER_SIDE = 6), or a Next click from the page straddling the seam clamps at the row end
  • the teleport is pure offsetLeft geometry, idempotent, and must not be gated on visibility flags — they lag a frame behind a teleport
  • arrows always enabled: useLeftArrowVisible/useRightArrowVisible track the outermost items, which here are clones, so they flash at the seam
  • useIsVisible OR'd across an item and both clone twins, same lag

normalize() also has to run after any manual scrollLeft write (a drag), not just on scrollend.

Center the clicked item

Items read the api from VisibilityContext — no apiRef needed inside the menu. scrollToItem takes an element or item object, never an id string.

tsx
function CenterCard({
  itemId,
  selected,
  onSelect,
}: {
  itemId: string;
  selected: boolean;
  onSelect: (id: string) => void;
}) {
  const api = React.useContext<publicApiType>(VisibilityContext);
  const handleClick = () => {
    onSelect(itemId);
    const el = api.getItemElementById(itemId);
    if (el) api.scrollToItem(el, 'smooth', 'center');
  };
  return (
    <div
      role="button"
      tabIndex={0}
      onClick={handleClick}
      onKeyDown={(ev) => ev.code === 'Enter' && handleClick()}
      style={{ width: 160, background: selected ? 'palegreen' : 'white' }}
    >
      {itemId}
    </div>
  );
}

Render CenterCard where Setup renders Card, holding the selected id in the parent — the menu itself needs no extra props.

Save and restore scroll position

Save scrollContainer.current.scrollLeft in onUpdate (fires after scroll settles — onScroll fires mid-animation), restore it in onInit.

tsx
export function RestoredMenu() {
  // Also set window.history.scrollRestoration = 'manual' once (an effect),
  // so the browser doesn't fight the restore on reload/back navigation.
  const save = (api: publicApiType) =>
    sessionStorage.setItem(
      'menu-pos',
      String(api.scrollContainer.current?.scrollLeft ?? 0),
    );

  const restore = (api: publicApiType) => {
    const node = api.scrollContainer.current;
    if (node) node.scrollLeft = +(sessionStorage.getItem('menu-pos') ?? 0);
  };
  return (
    <ScrollMenu
      LeftArrow={LeftArrow}
      RightArrow={RightArrow}
      onUpdate={save}
      onInit={restore}
    >
      {ids.map(renderCard)}
    </ScrollMenu>
  );
}

Load more when the end comes into view

Give the loader its own itemId as the last child; trigger fetching from onUpdate when the last item becomes visible.

tsx
const Loader = ({ itemId }: { itemId: string }) => (
  <div style={{ width: 160 }}>Loading…</div>
);

export function LoadMoreMenu() {
  const [items, setItems] = React.useState(ids);
  const [loading, setLoading] = React.useState(false);

  const fetchMore = async () => {
    setLoading(true);
    const next = await fetchPage(items.length); // your API
    setItems((cur) => [...cur, ...next]);
    setLoading(false);
  };
  return (
    <ScrollMenu
      LeftArrow={LeftArrow}
      RightArrow={RightArrow}
      onUpdate={(api) => {
        if (api.items.last()?.visible && !loading) fetchMore();
      }}
    >
      {items.map(renderCard)}
      {loading && <Loader itemId="loader" key="loader" />}
    </ScrollMenu>
  );
}

One item per scroll

scrollNext/scrollPrev page a full group of visible items; getPrevElement()/getNextElement() return the single item adjacent to the visible window.

tsx
function OneRightArrow() {
  const api = React.useContext<publicApiType>(VisibilityContext);
  const onClick = () => api.scrollToItem(api.getNextElement(), 'smooth', 'end');
  return (
    <button disabled={api.useRightArrowVisible()} onClick={onClick}>
      Next
    </button>
  );
}
// OneLeftArrow mirrors it: getPrevElement(), 'start', useLeftArrowVisible().

For custom group math, slidingWindow(...).next() and getItemsPos(group) are exported too — see skills/menu-scrolling/SKILL.md.

Snap to a card: CSS scroll-snap, current card from onScroll

Snapping is a stylesheet, not a prop: scroll-snap-type: x mandatory on the rail (scrollContainerClassName), scroll-snap-align: center on every item (itemClassName), side padding of calc(50% - card / 2) so the edge cards can center. The library's part is the current card: onScroll picks the item whose center is closest to the rail's center (offsetLeft against scrollLeft), and arrows and dots call scrollToItem(el, 'smooth', 'center') — a snap point by construction. The tilted fan of the neighbours is a CSS scroll-driven animation, and a mouse drag needs snap switched off for the gesture; the code for all three is in references/scroll-snap.md.

Hiding arrows, arrows below the menu, multiple menus, tabs

  • No arrows: LeftArrow/RightArrow are optional — omit them; the menu still scrolls natively (wheel, touch, drag recipes).
  • Arrows below the menu: render both arrows from a Footer slot; it reads VisibilityContext like arrows do (<ScrollMenu Footer={Arrows}>).
  • Multiple menus per page: each ScrollMenu is independent, but each must be seen on screen once before its visibility data is valid — below-the-fold menus show defaultValue states until scrolled into view.
  • Tab switching: remount with key={selectedTab} — see Common Mistakes.

Common Mistakes

CRITICAL Inventing autoplay/loop/snap props

Wrong:

tsx
<ScrollMenu autoplay autoplayInterval={3000} loop>
  {ids.map(renderCard)}
</ScrollMenu>

Correct:

tsx
// Autoplay: a timer firing scrollNext(), gated on menuVisible — recipe above.
// Loop: the useInfiniteLoop clone-and-teleport hook — references/infinite-loop.md
// Snap: CSS scroll-snap on the rail and items — recipe above.

These props do not exist and are silently ignored — the menu renders normally and never plays, loops or snaps. Only spring physics and slide effects are out of scope by design (use Embla or Swiper for those).

Source: README.md "What it does — and doesn't"; stories/Autoplay, stories/InfiniteLoop, stories/ScrollSnap

HIGH Drag-to-scroll on a mandatory snap container stutters, then jumps

Wrong: the plain drag recipe — scrollLeft += delta on every mousemove.

Correct:

tsx
// mousedown:  rail.style.scrollSnapType = 'none'
// mousemove:  rail.scrollLeft += delta
// mouseup:    scrollToItem(nearest card, 'smooth', 'center'), then
//             rail.style.scrollSnapType = '' once scrolling settles

A mandatory snap container snaps every programmatic scrollLeft write, and restoring snap while the release glide runs jumps instead of gliding.

Source: stories/ScrollSnap/ScrollSnap.source.tsx; references/scroll-snap.md

HIGH Autoplay interval scrolls the page to the menu

Wrong:

tsx
setInterval(() => apiRef.current?.scrollNext(), 3000);

Correct:

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

Scrolling is scrollIntoView-based, so scrollNext on an off-screen menu scrolls ancestors too — the page keeps jumping back to the menu every tick.

Source: stories/Autoplay/Autoplay.source.tsx:56-66; issue #276

HIGH Any programmatic scroll while the menu is off screen drags the page

Wrong:

tsx
onInit={(api) => api.scrollToItem(api.getItemById('item-9'), 'smooth')}
// menu below the fold: the whole page scrolls to the menu on load

Correct:

tsx
onUpdate={(api) => {
  if (api.items.getVisible().length && !api.isItemVisible('item-9')) {
    api.scrollToItem(api.getItemById('item-9'), 'smooth');
  }
}}

Same mechanism as autoplay above, for every recipe calling scroll methods outside a user gesture — gate on menuVisible.current or items.getVisible().

Source: issue #276 (#277, #174, #230); skills/menu-scrolling/SKILL.md

HIGH getItemById right after adding an item returns undefined

Wrong:

tsx
setItems([...items, newItem]);
apiRef.current.scrollToItem(apiRef.current.getItemById(newItem.id)); // undefined

Correct:

tsx
// after the state update commits (effect or onUpdate callback):
apiRef.current.scrollToItem(apiRef.current.getItemElementById(newItem.id));

The internal ItemsMap lags children by one render; getItemElementById queries the DOM by data-key and sees the new item immediately.

Source: issue #167; discussion #295; stories/AddItemAndScrollToIt

MEDIUM Promising a seamless infinite loop without clones

Wrong:

tsx
onUpdate={(api) => {
  if (api.items.last()?.visible) {
    api.scrollToItem(api.items.first(), 'smooth'); // visible rewind jump
  }
}}

Correct:

tsx
// Seamless needs cloned head/tail items plus a scrollLeft teleport at the
// seams — the useInfiniteLoop hook from references/infinite-loop.md:
const loop = useInfiniteLoop(ids);

Without pixel-identical clone zones the only possible "loop" is an animated scroll back to the first item — a visible jump, not a loop.

Source: issue #213; stories/InfiniteLoop/InfiniteLoop.source.tsx:94-113; stories/loopTestUtils.ts

MEDIUM Tab switching reuses one menu with stale position and state

Wrong:

tsx
<ScrollMenu LeftArrow={LeftArrow} RightArrow={RightArrow}>
  {tabs[selected].items.map(renderCard)}
</ScrollMenu>

Correct:

tsx
<ScrollMenu key={selected} LeftArrow={LeftArrow} RightArrow={RightArrow}>
  {tabs[selected].items.map(renderCard)}
</ScrollMenu>

Swapping the item set in place keeps the previous tab's scroll offset and visibility entries; key={selected} remounts the menu fresh per tab.

Source: discussion #294; issue #204

MEDIUM Load-more from scroll-position math instead of a loader item

Wrong:

tsx
onScroll={(api, ev) => {
  const el = ev.target as HTMLElement;
  if (el.scrollLeft > el.scrollWidth - 800) fetchMore();
}}

Correct:

tsx
onUpdate={(api) => {
  if (api.items.last()?.visible && !loading) fetchMore();
}}

Pixel thresholds break with dynamic item widths and onScroll fires mid-animation; the last item's visibility flag is layout-independent, and onUpdate fires only after the scroll settles.

Source: stories/AddItems/AddItems.source.tsx:54-59; discussion #297

See also

  • skills/menu-scrolling/SKILL.md — the imperative API every recipe is built from (scrollToItem, scrollNext/scrollPrev, apiRef).
  • skills/menu-interactions/SKILL.md — drag, wheel and body-scroll recipes; mouse/touch props are factories (api) => (event) => void.

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 Recipes AI skill do?

Recipes composed on the react-horizontal-scrolling-menu public API — NOT props: autoplay (setInterval + scrollNext gated on menuVisible), infinite loop/carousel (clone head/tail + scrollLeft teleport at seams), center on click (scrollToItem 'center'), save/restore scroll position (onUpdate, onInit, scrollContainer), load-more with a loader item (items.last()?.visible in onUpdate), one item per scroll (getPrevElement/getNextElement), snap to a card (CSS scroll-snap on scrollContainerClassName/itemClassName, current card from onScroll, drag with snap switched off), hiding arrows, multiple men...

Why use Menu Recipes on TypingMind?

Because you install it once and use it with any model. Menu Recipes 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 Recipes in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu/tree/master/skills/menu-recipes. 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 Recipes?

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 Recipes?

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

Is the Menu Recipes 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 👇