Game UI
Menu slider
A value on a menu row, never a native range input.
stablev0.5.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-sliderExamples
Default
API
<MenuSlider />A value on a menu row: a face's width, a colour's strength. ← → move it while the row has the cursor; ↑ ↓ stay the list's, to move to the next row. Built on Radix's slider — never a native `<input type="range">`, which in the FiveM NUI takes the keyboard from the game until it loses focus.
| Prop | Type | Default | Notes |
|---|---|---|---|
| label* | ReactNode | — | |
| onValueChange* | (value: number) => void | — | |
| value* | number | — | |
| className | string | — | |
| disabled | boolean | — | |
| format | ((value: number) => ReactNode) | — | How the value reads on the right: `(v) => \`${v}%\``. The number when left out. |
| max | number | 100 | |
| min | number | 0 | |
| step | number | 1 |
Dependencies
npm
- @radix-ui/react-slider@^1.2.2
Registry
- @fastrp/menu-list
- @fastrp/ui-sounds
- @fastrp/utils
Source
components/ds/menu-slider.tsxShowHide
'use client';
import * as SliderPrimitive from '@radix-ui/react-slider';
import * as React from 'react';
import {
focusOnHover,
menuRowClassName,
menuRowWithinClassName,
useMenuListNav,
} from '@/components/ds/menu-list';
import { useUiSound } from '@/components/ds/ui-sounds';
import { cn } from '@/lib/utils';
// @fast-ds [email protected]
export type MenuSliderProps = {
label: React.ReactNode;
value: number;
onValueChange: (value: number) => void;
min?: number;
max?: number;
step?: number;
/** How the value reads on the right: `(v) => \`${v}%\``. The number when left out. */
format?: (value: number) => React.ReactNode;
disabled?: boolean;
className?: string;
};
/**
* A value on a menu row: a face's width, a colour's strength. ← → move it while the row has the
* cursor; ↑ ↓ stay the list's, to move to the next row. Built on Radix's slider — never a native
* `<input type="range">`, which in the FiveM NUI takes the keyboard from the game until it loses
* focus.
*/
function MenuSlider({
label,
value,
onValueChange,
min = 0,
max = 100,
step = 1,
format,
disabled,
className,
}: MenuSliderProps) {
const nav = useMenuListNav();
const sound = useUiSound();
const lastTick = React.useRef(0);
return (
<div
data-slot="menu-slider"
className={cn(menuRowClassName, menuRowWithinClassName, 'gap-5', className)}
// Radix would take ↑ ↓ as "more / less"; in a menu they move between rows.
onKeyDownCapture={(event) => {
if (nav && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
event.preventDefault();
event.stopPropagation();
nav.move(event.target as Element, event.key === 'ArrowDown' ? 1 : -1);
}
}}
data-sfx-hover="navigate"
onPointerEnter={(event) => {
const thumb = event.currentTarget.querySelector<HTMLElement>('[role="slider"]');
if (event.pointerType === 'mouse' && thumb && document.activeElement !== thumb) {
thumb.focus({ preventScroll: true });
}
}}
>
<span className="w-[38%] min-w-0 shrink-0 truncate">{label}</span>
<SliderPrimitive.Root
value={[value]}
onValueChange={([next]) => {
if (next === undefined) return;
// A drag steps dozens of times a second; tick at most every 70ms, as a ratchet would.
const now = performance.now();
if (now - lastTick.current > 70) {
lastTick.current = now;
sound('adjust');
}
onValueChange(next);
}}
min={min}
max={max}
step={step}
disabled={disabled}
className="relative flex h-full flex-1 touch-none select-none items-center data-[disabled]:opacity-40"
>
<SliderPrimitive.Track className="relative h-1 flex-1 overflow-hidden rounded-full bg-foreground/20 group-focus-within/row:bg-background/20">
<SliderPrimitive.Range className="absolute h-full bg-foreground group-focus-within/row:bg-background" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb
data-menu-row
aria-label={typeof label === 'string' ? label : undefined}
onPointerEnter={focusOnHover}
className="block h-4 w-1.5 rounded-[1px] bg-foreground shadow-[0_0_0.5rem_rgb(0_0_0/0.5)] outline-none transition-transform duration-fast focus:scale-125 group-focus-within/row:bg-background"
/>
</SliderPrimitive.Root>
<span className="w-12 shrink-0 text-right tabular-nums">
{format ? format(value) : value}
</span>
</div>
);
}
export { MenuSlider };