Game UI

Menu view

The content that changes with a tab or a step in, sliding the way you went.

stablev0.4.0 Chromium 103 ui-scale safe

Install

Copies the file into components/ds/, with its registry dependencies and npm packages. See Installation for the one-time setup.
bunx shadcn@3 add @fastrp/menu-view

Examples

Default

Open alone

API

<MenuPresence />

A menu's arrival and departure. The pieces come in from their own edges (`MenuScreen` does that); on close everything leaves together, quickly — a game drops its menu at once.

PropTypeDefaultNotes
open*boolean—
classNamestring—
<MenuView />

The content that changes with a tab or a step into a sub-menu: the old view slips out one way and the new one in from the other, so the player always knows which way they went. The travel is a percentage of the view, so it scales with the NUI. Under reduced motion it only fades.

PropTypeDefaultNotes
viewKey*string—What is on screen: a tab's value, a drill-down's path. A new key plays the transition.
classNamestring—
direction1 | -11Where the new view comes from: `1` from the right (the next tab, a step in), `-1` from the left (the previous tab, a step back). The old view leaves the other way.

Dependencies

npm

  • @fastrp/design-tokens
  • motion@^12.40.0

Registry

  • None

Source

components/ds/menu-view.tsxShow
'use client';

import { DURATION_S, EASE } from '@fastrp/design-tokens/motion';
import { AnimatePresence, motion, useReducedMotion } from 'motion/react';
import type * as React from 'react';

// @fast-ds [email protected]

export type MenuViewProps = {
  /** What is on screen: a tab's value, a drill-down's path. A new key plays the transition. */
  viewKey: string;
  /**
   * Where the new view comes from: `1` from the right (the next tab, a step in), `-1` from the left
   * (the previous tab, a step back). The old view leaves the other way.
   */
  direction?: 1 | -1;
  children: React.ReactNode;
  className?: string;
};

/**
 * The content that changes with a tab or a step into a sub-menu: the old view slips out one way
 * and the new one in from the other, so the player always knows which way they went. The travel
 * is a percentage of the view, so it scales with the NUI. Under reduced motion it only fades.
 */
function MenuView({ viewKey, direction = 1, children, className }: MenuViewProps) {
  const reduceMotion = useReducedMotion();
  const travel = reduceMotion ? 0 : 6;

  return (
    <AnimatePresence mode="wait" initial={false} custom={direction}>
      <motion.div
        key={viewKey}
        data-slot="menu-view"
        custom={direction}
        variants={{
          enter: (d: number) => ({ opacity: 0, x: `${d * travel}%` }),
          center: { opacity: 1, x: '0%' },
          exit: (d: number) => ({ opacity: 0, x: `${-d * travel}%` }),
        }}
        initial="enter"
        animate="center"
        exit="exit"
        transition={{ duration: DURATION_S.base, ease: EASE }}
        className={className}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  );
}

export type MenuPresenceProps = {
  open: boolean;
  children: React.ReactNode;
  className?: string;
};

/**
 * A menu's arrival and departure. The pieces come in from their own edges (`MenuScreen` does
 * that); on close everything leaves together, quickly — a game drops its menu at once.
 */
function MenuPresence({ open, children, className }: MenuPresenceProps) {
  return (
    <AnimatePresence>
      {open && (
        <motion.div
          key="menu"
          className={className}
          initial={{ opacity: 1 }}
          exit={{ opacity: 0, scale: 0.985 }}
          transition={{ duration: DURATION_S.fast + 0.06, ease: EASE }}
        >
          {children}
        </motion.div>
      )}
    </AnimatePresence>
  );
}

export { MenuPresence, MenuView };