A styled native HTML select element for simple lists and mobile-friendly forms.
Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/native-select.jsonInstalls to @/components/ui/native-select.
Manual installation
Install the dependencies:
pnpm add lucide-react@^1.48.0Copy the source into your project:
import { ChevronDownIcon } from 'lucide-react'
import type * as React from 'react'
import { cn } from '@/lib/utils'
type NativeSelectProps = Omit<React.ComponentProps<'select'>, 'size'> & {
size?: 'sm' | 'default'
}
function NativeSelect({ className, size = 'default', ...props }: NativeSelectProps) {
return (
<div
className={cn('group/native-select relative w-fit has-[select:disabled]:opacity-50', className)}
data-slot="native-select-wrapper"
data-size={size}
>
<select
data-slot="native-select"
data-size={size}
className="h-10 w-full min-w-0 appearance-none rounded-lg border border-input bg-transparent py-1 pr-8 pl-2.5 text-sm transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-8 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40"
{...props}
/>
<ChevronDownIcon
className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none"
aria-hidden="true"
data-slot="native-select-icon"
/>
</div>
)
}
function NativeSelectOption({ className, ...props }: React.ComponentProps<'option'>) {
return (
<option data-slot="native-select-option" className={cn('bg-[Canvas] text-[CanvasText]', className)} {...props} />
)
}
function NativeSelectOptGroup({ className, ...props }: React.ComponentProps<'optgroup'>) {
return (
<optgroup data-slot="native-select-optgroup" className={cn('bg-[Canvas] text-[CanvasText]', className)} {...props} />
)
}
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }A styled native HTML select element. Use it when you want the platform picker (for example on mobile devices), native form submission or maximum robustness, instead of the custom Select popover.
Features
- Native select behaviour and form submission
- Sizes: default and sm
- Option groups
- Disabled and invalid states
- Consistent styling with other inputs
Usage
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
export function CaseTypeSelect() {
return (
<NativeSelect defaultValue="">
<NativeSelectOption value="" disabled>
Select case type
</NativeSelectOption>
<NativeSelectOption value="building-permit">Building permit</NativeSelectOption>
<NativeSelectOption value="parking-permit">Parking permit</NativeSelectOption>
</NativeSelect>
)
}Examples
Default
A case type picker with a label and a description.
With Groups
Departments organised in option groups.
Sizes
The default and sm sizes.
Filter Toolbar
Small selects used as filters above a case list. Each select has a visually hidden label.
In a Form
A school placement application with several selects in a card.
Disabled
A locked case status.
Invalid
A required parking zone with an error message.
Accessibility
- Uses the native select element with full platform accessibility
- Associate a label with htmlFor and id; use a visually hidden label when the context makes it obvious
- Set aria-invalid for invalid values and describe the error in text
- The chevron icon is aria-hidden