OptionsSelect
Typed options wrapper around the UI select primitive.
Recipe Source
"use client";import type { Select as SelectPrimitive } from "@base-ui/react/select";import { Select, SelectContent, SelectItem, type SelectSize, SelectTrigger, SelectValue,} from "@giddaa-housing/ui/select";import type { ReactNode } from "react";import { cn } from "./lib/cn";import { getSelectableOptionKey, isSelectableOptionValueEqual, type SelectableOption, type SelectableOptionValue,} from "./selectable-option";type OptionsSelectProps<Option extends SelectableOption = SelectableOption> = Omit< SelectPrimitive.Root.Props<string>, | "children" | "defaultValue" | "items" | "multiple" | "onValueChange" | "size" | "value" > & { contentClassName?: string; contentProps?: Omit< React.ComponentProps<typeof SelectContent>, "children" | "className" >; defaultValue?: SelectableOptionValue | null; emptyMessage?: ReactNode; invalid?: boolean; itemClassName?: string; onValueChange?: ( value: Option["value"] | null, option: Option | null, ) => void; options: readonly Option[]; placeholder?: ReactNode; size?: SelectSize; triggerClassName?: string; value?: SelectableOptionValue | null; };function OptionsSelect<Option extends SelectableOption = SelectableOption>({ contentClassName, contentProps, defaultValue, emptyMessage = "No options available", invalid = false, itemClassName, onValueChange, options, placeholder = "Select an option", size, triggerClassName, value, ...props}: OptionsSelectProps<Option>) { const selectItems = options.map((option) => ({ label: option.label, value: getSelectableOptionKey(option), })); const valueProps = value === undefined ? {} : { value: value == null ? null : String(value), }; const defaultValueProps = defaultValue === undefined ? {} : { defaultValue: defaultValue == null ? null : String(defaultValue), }; return ( <Select<string> items={selectItems} size={size} {...valueProps} {...defaultValueProps} onValueChange={(nextValue) => { if (nextValue == null) { onValueChange?.(null, null); return; } const option = findOptionByValue(options, nextValue); onValueChange?.(option?.value ?? null, option); }} {...props} > <SelectTrigger className={cn("w-full", triggerClassName)} aria-invalid={invalid || undefined} data-invalid={invalid || undefined} > <SelectValue placeholder={placeholder} /> </SelectTrigger> <SelectContent className={contentClassName} {...contentProps}> {options.length === 0 ? ( <div className="px-3 py-2 text-gdt-sm text-fg-secondary"> {emptyMessage} </div> ) : ( options.map((option) => ( <SelectItem key={getSelectableOptionKey(option)} value={getSelectableOptionKey(option)} disabled={option.disabled} className={itemClassName} > {option.label} </SelectItem> )) )} </SelectContent> </Select> );}function findOptionByValue<Option extends SelectableOption>( options: readonly Option[], value: SelectableOptionValue | null,) { if (value == null) { return null; } return ( options.find((option) => isSelectableOptionValueEqual(option.value, value), ) ?? null );}export { OptionsSelect, type OptionsSelectProps };Usage
import { OptionsSelect } from "@/components/recipes/options-select";
<OptionsSelect options={[{ label: "Standard", value: "standard" }]} />Examples
Options Select
Use this for simple option lists where consumers do not need to hand-code SelectItem elements.
Props
| Prop/API | Type | Default | Description |
|---|---|---|---|
options | SelectableOption[] | - | Options to render. |
value / defaultValue | option value | - | Controlled or uncontrolled selected value. |
placeholder | ReactNode | "Select an option" | Placeholder shown before selection. |
invalid | boolean | false | Marks the trigger invalid. |
Accessibility
- Provide useful option labels.
- Use
invalidwith visible error text from surrounding form UI. - Keep disabled options non-interactive.