Component
Select
A DOM select — never the native one, which the NUI cannot show.
stablev0.3.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/selectExamples
Default
API
<Select />Takes the props of what it wrapsRadix's select, audible: `open` and `close` with the list, `select` on a pick — the pick swallows the `close` that follows it.
<SelectItem />Takes the props of what it wrapsAn option; the highlight moving onto it — by key or pointer — ticks `navigate`.
<SelectTrigger />Never a native `<select>`: its popup is drawn by the OS outside the page, which the FiveM NUI cannot show, and it takes the game's keyboard focus. This one is DOM all the way down.
| Prop | Type | Default | Notes |
|---|---|---|---|
| size | "sm" | "md" | md |
Dependencies
npm
- @radix-ui/react-select@^2.1.4
- lucide-react@^0.469.0
Registry
- @fastrp/ui-sounds
- @fastrp/utils
Source
components/ds/select.tsxShowHide
'use client';
import * as SelectPrimitive from '@radix-ui/react-select';
import { Check, ChevronDown, ChevronUp } from 'lucide-react';
import type * as React from 'react';
import { useSoundedOpen, useUiSound } from '@/components/ds/ui-sounds';
import { cn } from '@/lib/utils';
// @fast-ds [email protected]
/**
* Radix's select, audible: `open` and `close` with the list, `select` on a pick — the pick swallows
* the `close` that follows it.
*/
function Select({
open,
defaultOpen,
onOpenChange,
onValueChange,
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
const sound = useUiSound();
return (
<SelectPrimitive.Root
{...props}
{...useSoundedOpen({ open, defaultOpen, onOpenChange })}
onValueChange={(value) => {
sound('select');
onValueChange?.(value);
}}
/>
);
}
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
/**
* Never a native `<select>`: its popup is drawn by the OS outside the page, which the FiveM NUI
* cannot show, and it takes the game's keyboard focus. This one is DOM all the way down.
*/
function SelectTrigger({
className,
children,
size = 'md',
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & { size?: 'sm' | 'md' }) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
'flex w-full min-w-0 items-center justify-between gap-2 whitespace-nowrap rounded-md border border-foreground/10 bg-ink/60 px-3 text-sm text-foreground shadow-[inset_0_1px_2px_rgb(0_0_0/0.35)] transition-[border-color,box-shadow] duration-fast hover:border-foreground/20 focus-visible:border-primary/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/25 data-[state=open]:border-primary/70 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-danger data-[placeholder]:text-muted-foreground data-[size=md]:h-10 data-[size=sm]:h-8 [&>span]:truncate [&_svg]:size-4 [&_svg]:shrink-0',
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="text-muted-foreground" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
'flex cursor-default items-center justify-center py-1 [&_svg]:size-4',
className,
)}
{...props}
>
<ChevronUp />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
'flex cursor-default items-center justify-center py-1 [&_svg]:size-4',
className,
)}
{...props}
>
<ChevronDown />
</SelectPrimitive.ScrollDownButton>
);
}
function SelectContent({
className,
children,
position = 'popper',
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
position={position}
className={cn(
'panel relative z-popover max-h-[var(--radix-select-content-available-height)] min-w-[8rem] origin-[var(--radix-select-content-transform-origin)] overflow-y-auto overflow-x-hidden rounded-lg text-popover-foreground data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1',
className,
)}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1',
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn('px-2 py-1.5 text-eyebrow uppercase text-muted-foreground', className)}
{...props}
/>
);
}
/** An option; the highlight moving onto it — by key or pointer — ticks `navigate`. */
function SelectItem({
className,
children,
onFocus,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
const sound = useUiSound();
return (
<SelectPrimitive.Item
data-slot="select-item"
onFocus={(event) => {
sound('navigate');
onFocus?.(event);
}}
className={cn(
'relative flex w-full cursor-default select-none items-center gap-2 rounded-md py-2 pl-2.5 pr-8 text-sm outline-none transition-colors duration-fast focus:bg-foreground focus:text-background data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0',
className,
)}
{...props}
>
<span className="absolute right-2 flex size-4 items-center justify-center text-brand [[data-highlighted]_&]:text-background">
<SelectPrimitive.ItemIndicator>
<Check />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn('-mx-1 my-1 h-px bg-foreground/10', className)}
{...props}
/>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};