NarraLeaf

Quick Menu

Scope

The Quick Menu is a context panel that lets the player access common actions (stepping back and forward, history, auto, save/load, settings, exit) with one click or key.

The component below covers stepping back and forward, history, auto-forward, save, load, settings, and exit. Route names and exit behavior remain application code.

undo() and redo() return false when there is nowhere to go, so the buttons above are safe to press at either end of the timeline. Use canUndo() / canRedo() to disable them instead.

1. Create the component

import { useGame, usePreference, useRouter } from "narraleaf-react";
import { ArrowLeft, ArrowRight, History, Play, Save, FileText, Settings, Home } from "lucide-react";

export default function QuickMenu() {
  const game = useGame();
  const router = useRouter();
  const liveGame = game.getLiveGame();
  const [autoForward, setAutoForward] = usePreference("autoForward");

  // helpers ------------------------------------------------
  const undo = () => liveGame.undo();
  const redo = () => liveGame.redo();
  const toHistory = () => router.navigate("/history");
  const toggleAuto = () => setAutoForward(!autoForward);
  const save = () => router.navigate("/save");
  const load = () => router.navigate("/load");
  const openSettings = () => router.navigate("/settings");
  const exitGame = () => /* your own exit logic (e.g. useApp().exitGame()) */ undefined;

  // menu item ---------------------------------------------
  const Item = ({ icon: Icon, label, onClick }: { icon: any; label: string; onClick: () => void }) => (
    <button onClick={onClick} className="flex items-center gap-1 px-2 py-1 rounded-full hover:bg-white/20">
      <Icon className="w-4 h-4" />
      <span className="text-xs">{label}</span>
    </button>
  );

  return (
    <div className="fixed bottom-5 left-0 right-0 flex justify-center pointer-events-none">
      <div className="flex gap-2 bg-black/40 rounded-full px-4 py-1 pointer-events-auto">
        <Item icon={ArrowLeft} label="Back" onClick={undo} />
        <Item icon={ArrowRight} label="Forward" onClick={redo} />
        <Item icon={History} label="History" onClick={toHistory} />
        <Item icon={Play} label={autoForward ? "Auto: On" : "Auto: Off"} onClick={toggleAuto} />
        <Item icon={Save} label="Save" onClick={save} />
        <Item icon={FileText} label="Load" onClick={load} />
        <Item icon={Settings} label="Settings" onClick={openSettings} />
        <Item icon={Home} label="Exit" onClick={exitGame} />
      </div>
    </div>
  );
}

2. Use in LayoutRouter

Place the menu in the root layout's default page (/). Player creates the root layout automatically.

import { GameProviders, Page, Player } from "narraleaf-react";
import QuickMenu from "./QuickMenu";

function MyApp() {
  return (
    <GameProviders>
      <Player story={story} onReady={({ liveGame }) => liveGame.newGame()}>
        {/* QuickMenu will be displayed when the router is at `/`, which is the default page */}
        {/* You can also use `router.clear().navigate("/")` to display the quick menu in-game */}
        {/* This means the quick menu will be displayed when all other pages are closed */}
        <Page name={null}>
          <QuickMenu />
        </Page>
      </Player>
    </GameProviders>
  );
}

3. Transitions

The component above is static. Turn the top-level element into a Motion element to add enter and exit animation. The page router keeps the component mounted until its exit animation finishes.

import { motion } from "motion/react";

export default function QuickMenu() {
  // ...hooks & helpers...

  return (
    <motion.div
      /* initial ➜ before enter */
      initial={{ opacity: 0, scale: 0.8, y: 20 }}
      /* animate ➜ after enter */
      animate={{ opacity: 1, scale: 1, y: 0 }}
      /* exit ➜ before unmount */
      exit={{ opacity: 0, scale: 0.8, y: 20 }}
      /* timing curve */
      transition={{ type: "spring", stiffness: 300, damping: 25, duration: 0.3 }}
      className="fixed bottom-5 left-0 right-0 flex justify-center pointer-events-none"
    >
      <div className="flex gap-2 bg-black/40 rounded-full px-4 py-1 pointer-events-auto">
        {/* menu items */}
      </div>
    </motion.div>
  );
}

Guidelines:

  • Root motion element – Only the top-level node needs to be a motion.* element; inner content can stay regular JSX.
  • Customize initial / animate / exit to achieve fade, slide, scale, etc.
  • Staggered items – For more elaborate effects, wrap each button in its own motion.button or use Motion variants.
  • Presence management – The NarraLeaf page router already manages presence for Page content. Add your own <AnimatePresence> only when conditionally rendering outside a Page.

On this page