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-viewExamples
Default
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.
| Prop | Type | Default | Notes |
|---|---|---|---|
| open* | boolean | — | |
| className | string | — |
<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.
| Prop | Type | Default | Notes |
|---|---|---|---|
| viewKey* | string | — | What is on screen: a tab's value, a drill-down's path. A new key plays the transition. |
| className | string | — | |
| direction | 1 | -1 | 1 | 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. |
Dependencies
npm
- @fastrp/design-tokens
- motion@^12.40.0
Registry
- None
Source
components/ds/menu-view.tsxShowHide
'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 };