Forms
Native Select.
A styled native select element with the Input look, a chevron, options and option groups.
import { Label } from "@/components/ui/label";
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
export default function NativeSelectDemo() {
return (
<div className="grid w-full max-w-xs gap-2.5">
<Label htmlFor="ns-world">World</Label>
<NativeSelect id="ns-world" defaultValue="" className="w-full">
<NativeSelectOption value="" disabled>
Pick a world
</NativeSelectOption>
<NativeSelectOption value="overworld">Overworld</NativeSelectOption>
<NativeSelectOption value="deepstone">Deepstone</NativeSelectOption>
<NativeSelectOption value="skylands">Skylands</NativeSelectOption>
</NativeSelect>
</div>
);
}Installation#
With the CLI
$npx lanterncn add native-selectThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/native-select. See Installation if your project is not set up yet.
Manually
Install the dependencies
npm install lucide-reactCopy the source into your project
components/ui/native-select.tsximport * as React from "react"; import { ChevronDownIcon } from "lucide-react"; import { cn } from "@/lib/utils"; function NativeSelect({ className, size = "default", ...props }: Omit<React.ComponentProps<"select">, "size"> & { size?: "sm" | "default" }) { return ( <div data-slot="native-select-wrapper" className="group/native-select relative w-fit has-[select:disabled]:opacity-50" > <select data-slot="native-select" data-size={size} className={cn( "h-10 w-full min-w-0 cursor-pointer appearance-none rounded-md border border-input bg-background/40 px-3 py-2 pr-9 text-sm text-foreground transition-[border-color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground", "data-[size=sm]:h-8 data-[size=sm]:py-1 data-[size=sm]:text-xs", "hover:border-muted-foreground/60 focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-ring/25", "aria-invalid:border-destructive aria-invalid:ring-destructive/20 disabled:pointer-events-none disabled:cursor-not-allowed", className, )} {...props} /> <ChevronDownIcon data-slot="native-select-icon" aria-hidden="true" className="pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-muted-foreground opacity-80 select-none group-has-[select:focus-visible]/native-select:text-primary group-has-[select[aria-invalid=true]]/native-select:text-destructive" /> </div> ); } function NativeSelectOption({ className, ...props }: React.ComponentProps<"option">) { return <option data-slot="native-select-option" className={cn("bg-popover text-popover-foreground", className)} {...props} />; } function NativeSelectOptGroup({ className, ...props }: React.ComponentProps<"optgroup">) { return ( <optgroup data-slot="native-select-optgroup" className={cn("bg-popover text-popover-foreground", className)} {...props} /> ); } export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };Update the import paths to match your project
The source imports
cnfrom@/lib/utils.
Usage#
import { NativeSelect, NativeSelectOptGroup, NativeSelectOption } from "@/components/ui/native-select";<NativeSelect defaultValue="overworld">
<NativeSelectOption value="overworld">Overworld</NativeSelectOption>
<NativeSelectOption value="deepstone">Deepstone</NativeSelectOption>
</NativeSelect>Use Native Select when you want the platform picker, especially on phones, or need the value to work in a plain form post. Use Select for a fully styled dropdown with icons.
The wrapper is w-fit by default. Pass className="w-full" to stretch the select; the open list is drawn by the browser.
Examples#
Groups
import { Label } from "@/components/ui/label";
import { NativeSelect, NativeSelectOptGroup, NativeSelectOption } from "@/components/ui/native-select";
export default function NativeSelectGroups() {
return (
<div className="grid w-full max-w-xs gap-2.5">
<Label htmlFor="ns-computer">Computer</Label>
<NativeSelect id="ns-computer" defaultValue="turtle-03">
<NativeSelectOptGroup label="Turtles">
<NativeSelectOption value="turtle-01">Turtle T-01</NativeSelectOption>
<NativeSelectOption value="turtle-02">Turtle T-02</NativeSelectOption>
<NativeSelectOption value="turtle-03">Turtle T-03</NativeSelectOption>
</NativeSelectOptGroup>
<NativeSelectOptGroup label="Terminals">
<NativeSelectOption value="hub">Hub terminal</NativeSelectOption>
<NativeSelectOption value="farm">Farm monitor</NativeSelectOption>
</NativeSelectOptGroup>
</NativeSelect>
</div>
);
}Invalid, disabled and small
Choose a region before publishing.
import { Label } from "@/components/ui/label";
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
export default function NativeSelectStates() {
return (
<div className="grid w-full max-w-xs gap-6">
<div className="grid gap-2.5">
<Label htmlFor="ns-invalid" className="text-destructive">
Region
</Label>
<NativeSelect id="ns-invalid" aria-invalid="true" aria-describedby="ns-invalid-error" defaultValue="" className="w-full">
<NativeSelectOption value="">Pick a region</NativeSelectOption>
<NativeSelectOption value="north">North shelf</NativeSelectOption>
<NativeSelectOption value="south">South marsh</NativeSelectOption>
</NativeSelect>
<p id="ns-invalid-error" className="text-[13px] text-destructive">
Choose a region before publishing.
</p>
</div>
<div className="grid gap-2.5">
<Label htmlFor="ns-disabled">Server</Label>
<NativeSelect id="ns-disabled" disabled defaultValue="deepstone" className="w-full">
<NativeSelectOption value="deepstone">Deepstone</NativeSelectOption>
</NativeSelect>
</div>
<div className="grid gap-2.5">
<Label htmlFor="ns-small">Sort by</Label>
<NativeSelect id="ns-small" size="sm" defaultValue="recent">
<NativeSelectOption value="recent">Most recent</NativeSelectOption>
<NativeSelectOption value="visits">Most visited</NativeSelectOption>
</NativeSelect>
</div>
</div>
);
}