OptionsSelect

Typed options wrapper around the UI select primitive.

Recipe Source

components/ui/options-select.tsx
"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/APITypeDefaultDescription
optionsSelectableOption[]-Options to render.
value / defaultValueoption value-Controlled or uncontrolled selected value.
placeholderReactNode"Select an option"Placeholder shown before selection.
invalidbooleanfalseMarks the trigger invalid.

Accessibility

  • Provide useful option labels.
  • Use invalid with visible error text from surrounding form UI.
  • Keep disabled options non-interactive.

On this page