Native Select
The browser select with an Edmi skin. Use on mobile and for long, simple lists.
import { NativeSelect, NativeSelectOptGroup, NativeSelectOption,} from "@/components/ui/native-select";
export default function Demo() { return ( <div className="flex flex-wrap items-start gap-4"> <NativeSelect defaultValue="devnet" className="w-48"> <NativeSelectOption value="devnet">Solana devnet</NativeSelectOption> <NativeSelectOption value="testnet">Solana testnet</NativeSelectOption> <NativeSelectOptGroup label="Production"> <NativeSelectOption value="mainnet">Mainnet</NativeSelectOption> </NativeSelectOptGroup> </NativeSelect> <NativeSelect disabled className="w-48"> <NativeSelectOption>Mainnet</NativeSelectOption> </NativeSelect> <NativeSelect aria-invalid size="sm" className="w-48"> <NativeSelectOption value="">Required</NativeSelectOption> </NativeSelect> </div> );}<script setup lang="ts">import { NativeSelect, NativeSelectOptGroup, NativeSelectOption,} from "@/components/ui/native-select";</script>
<template> <div class="flex flex-wrap items-start gap-4"> <NativeSelect model-value="devnet" class="w-48"> <NativeSelectOption value="devnet">Solana devnet</NativeSelectOption> <NativeSelectOption value="testnet">Solana testnet</NativeSelectOption> <NativeSelectOptGroup label="Production"> <NativeSelectOption value="mainnet">Mainnet</NativeSelectOption> </NativeSelectOptGroup> </NativeSelect> <NativeSelect disabled class="w-48"> <NativeSelectOption>Mainnet</NativeSelectOption> </NativeSelect> <NativeSelect aria-invalid="true" size="sm" class="w-48"> <NativeSelectOption value="">Required</NativeSelectOption> </NativeSelect> </div></template><script lang="ts"> import { NativeSelect, NativeSelectOptGroup, NativeSelectOption, } from "$lib/components/ui/native-select";
let network = $state("devnet");</script>
<div class="flex flex-wrap items-start gap-4"> <NativeSelect bind:value={network} class="w-48"> <NativeSelectOption value="devnet">Solana devnet</NativeSelectOption> <NativeSelectOption value="testnet">Solana testnet</NativeSelectOption> <NativeSelectOptGroup label="Production"> <NativeSelectOption value="mainnet">Mainnet</NativeSelectOption> </NativeSelectOptGroup> </NativeSelect> <NativeSelect disabled class="w-48"> <NativeSelectOption>Mainnet</NativeSelectOption> </NativeSelect> <NativeSelect aria-invalid="true" size="sm" class="w-48"> <NativeSelectOption value="">Required</NativeSelectOption> </NativeSelect></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/native-selectpnpm dlx shadcn@latest add @edmi-ui/native-selectyarn dlx shadcn@latest add @edmi-ui/native-selectbunx shadcn@latest add @edmi-ui/native-selectPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/native-selectpnpm dlx shadcn-vue@latest add @edmi-ui/native-selectyarn dlx shadcn-vue@latest add @edmi-ui/native-selectbunx shadcn-vue@latest add @edmi-ui/native-selectPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @vueuse/coreTerminal window pnpm add @vueuse/coreTerminal window yarn add @vueuse/coreTerminal window bun add @vueuse/coreAdd the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/native-select.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/native-select.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/native-select.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/native-select.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { NativeSelect, NativeSelectOptGroup, NativeSelectOption,} from "@/components/ui/native-select";
<div className="flex flex-wrap items-start gap-4"> <NativeSelect defaultValue="devnet" className="w-48"> <NativeSelectOption value="devnet">Solana devnet</NativeSelectOption> <NativeSelectOption value="testnet">Solana testnet</NativeSelectOption> <NativeSelectOptGroup label="Production"> <NativeSelectOption value="mainnet">Mainnet</NativeSelectOption> </NativeSelectOptGroup> </NativeSelect> <NativeSelect disabled className="w-48"> <NativeSelectOption>Mainnet</NativeSelectOption> </NativeSelect> <NativeSelect aria-invalid size="sm" className="w-48"> <NativeSelectOption value="">Required</NativeSelectOption> </NativeSelect></div><script setup lang="ts">import { NativeSelect, NativeSelectOptGroup, NativeSelectOption,} from "@/components/ui/native-select";</script>
<template> <div class="flex flex-wrap items-start gap-4"> <NativeSelect model-value="devnet" class="w-48"> <NativeSelectOption value="devnet">Solana devnet</NativeSelectOption> <NativeSelectOption value="testnet">Solana testnet</NativeSelectOption> <NativeSelectOptGroup label="Production"> <NativeSelectOption value="mainnet">Mainnet</NativeSelectOption> </NativeSelectOptGroup> </NativeSelect> <NativeSelect disabled class="w-48"> <NativeSelectOption>Mainnet</NativeSelectOption> </NativeSelect> <NativeSelect aria-invalid="true" size="sm" class="w-48"> <NativeSelectOption value="">Required</NativeSelectOption> </NativeSelect> </div></template><script lang="ts"> import { NativeSelect, NativeSelectOptGroup, NativeSelectOption, } from "$lib/components/ui/native-select";</script>
<div class="flex flex-wrap items-start gap-4"> <NativeSelect class="w-48"> <NativeSelectOption value="devnet">Solana devnet</NativeSelectOption> <NativeSelectOption value="testnet">Solana testnet</NativeSelectOption> <NativeSelectOptGroup label="Production"> <NativeSelectOption value="mainnet">Mainnet</NativeSelectOption> </NativeSelectOptGroup> </NativeSelect> <NativeSelect disabled class="w-48"> <NativeSelectOption>Mainnet</NativeSelectOption> </NativeSelect> <NativeSelect aria-invalid="true" size="sm" class="w-48"> <NativeSelectOption value="">Required</NativeSelectOption> </NativeSelect></div>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type * as React from "react";import { CaretDownIcon } from "@phosphor-icons/react";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
// ✦ depth (v4): fields sink (-1) in layered mode; focus swaps the edge for the ringconst fieldElevation = { sunken: "border-sk-bd bg-sk-bg shadow-sunken focus-visible:bg-card", flat: "", raised: "border-transparent bg-[image:linear-gradient(var(--bv-face-b),var(--bv-face-b))] shadow-raised focus-visible:border-ring focus-visible:shadow-ring", floating: "border-transparent bg-[image:linear-gradient(var(--bv-face-b),var(--bv-face-b))] shadow-floating focus-visible:border-ring focus-visible:shadow-ring",};
type NativeSelectProps = Omit<React.ComponentProps<"select">, "size"> & { size?: "sm" | "default"; /** ✦ depth: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation;};
function NativeSelect({ className, size = "default", elevation, ...props}: NativeSelectProps) { const level = useElevation(elevation, "field"); 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={cn( "h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-card pr-8 pl-3 text-sm text-foreground outline-none select-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:shadow-ring disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-muted aria-invalid:border-destructive aria-invalid:shadow-ring-error data-[size=sm]:h-8 data-[size=sm]:rounded-[7px]", fieldElevation[level], )} {...props} /> <CaretDownIcon 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 };<script setup lang="ts">import type { AcceptableValue } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { ChevronDownIcon } from '@lucide/vue'import { reactiveOmit, useVModel } from '@vueuse/core'import { cn } from '@/registry/edmi/lib/utils'import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'
defineOptions({ inheritAttrs: false,})
const props = withDefaults(defineProps<{ modelValue?: AcceptableValue | AcceptableValue[] size?: 'sm' | 'default' /** ✦ depth: sunken -1, flat 0, raised +1, floating +2 */ elevation?: Elevation class?: HTMLAttributes['class']}>(), { size: 'default', elevation: undefined,})
const emit = defineEmits<{ 'update:modelValue': AcceptableValue}>()
const modelValue = useVModel(props, 'modelValue', emit, { passive: true, defaultValue: '',})
const delegatedProps = reactiveOmit(props, 'class', 'size', 'elevation')// ✦ depth (v4): fields sink (-1) in layered mode; focus swaps the edge for the ringconst fieldElevation = { sunken: 'border-sk-bd bg-sk-bg shadow-sunken focus-visible:bg-card', flat: '', raised: 'border-transparent bg-[image:linear-gradient(var(--bv-face-b),var(--bv-face-b))] shadow-raised focus-visible:border-ring focus-visible:shadow-ring', floating: 'border-transparent bg-[image:linear-gradient(var(--bv-face-b),var(--bv-face-b))] shadow-floating focus-visible:border-ring focus-visible:shadow-ring',}
const level = useElevation(() => props.elevation, 'field')</script>
<template> <div :class="cn('group/native-select relative w-fit has-[select:disabled]:opacity-50', props.class)" data-slot="native-select-wrapper" :data-size="size" > <select v-bind="{ ...$attrs, ...delegatedProps }" v-model="modelValue" data-slot="native-select" :data-size="size" :class="cn('h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-card pr-8 pl-3 text-sm text-foreground outline-none select-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:shadow-ring disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-muted aria-invalid:border-destructive aria-invalid:shadow-ring-error data-[size=sm]:h-8 data-[size=sm]:rounded-[7px]', fieldElevation[level])" > <slot /> </select> <ChevronDownIcon class="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></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class'] }>()</script>
<template> <optgroup data-slot="native-select-optgroup" :class="cn('bg-[Canvas] text-[CanvasText]', props.class)"> <slot /> </optgroup></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class'] }>()</script>
<template> <option data-slot="native-select-option" :class="cn('bg-[Canvas] text-[CanvasText]', props.class)"> <slot /> </option></template>export { default as NativeSelect } from "./NativeSelect.vue";export { default as NativeSelectOptGroup } from "./NativeSelectOptGroup.vue";export { default as NativeSelectOption } from "./NativeSelectOption.vue";<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLOptgroupAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLOptgroupAttributes> = $props();</script>
<optgroup bind:this={ref} data-slot="native-select-optgroup" class={cn("bg-[Canvas] text-[CanvasText]", className)} {...restProps}> {@render children?.()}</optgroup><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLOptionAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLOptionAttributes> = $props();</script>
<option bind:this={ref} data-slot="native-select-option" class={cn("bg-[Canvas] text-[CanvasText]", className)} {...restProps}> {@render children?.()}</option><script lang="ts"> import CaretDownIcon from "phosphor-svelte/lib/CaretDown"; import { cn, type WithElementRef } from "$lib/utils.js"; import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js"; import type { HTMLSelectAttributes } from "svelte/elements";
type NativeSelectProps = Omit<WithElementRef<HTMLSelectAttributes>, "size"> & { size?: "sm" | "default"; /** ✦ depth: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation; };
let { ref = $bindable(null), value = $bindable(), class: className, size = "default", elevation = "auto", children, ...restProps }: NativeSelectProps = $props(); // ✦ depth (v4): fields sink (-1) in layered mode; focus swaps the edge for the ring const fieldElevation = { sunken: "border-sk-bd bg-sk-bg shadow-sunken focus-visible:bg-card", flat: "", raised: "border-transparent bg-[image:linear-gradient(var(--bv-face-b),var(--bv-face-b))] shadow-raised focus-visible:border-ring focus-visible:shadow-ring", floating: "border-transparent bg-[image:linear-gradient(var(--bv-face-b),var(--bv-face-b))] shadow-floating focus-visible:border-ring focus-visible:shadow-ring", };
const level = useElevation(() => elevation, "field");</script>
<div class={cn( "group/native-select relative w-fit has-[select:disabled]:opacity-50", className )} data-slot="native-select-wrapper" data-size={size}> <select bind:value bind:this={ref} data-slot="native-select" data-size={size} class={cn( "h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-card pr-8 pl-3 text-sm text-foreground outline-none select-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:shadow-ring disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-muted aria-invalid:border-destructive aria-invalid:shadow-ring-error data-[size=sm]:h-8 data-[size=sm]:rounded-[7px]", fieldElevation[level.current] )} {...restProps} > {@render children?.()} </select> <CaretDownIcon class="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none" aria-hidden data-slot="native-select-icon" /></div>import Root from "./native-select.svelte";import OptGroup from "./native-select-opt-group.svelte";import Option from "./native-select-option.svelte";
export { OptGroup, OptGroup as NativeSelectOptGroup, Option, Option as NativeSelectOption, Root, Root as NativeSelect,};Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Same levels as Input, applied to the select box: sunken (-1), flat, raised (+1), floating (+2).
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
const levels = [ { value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-5"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <NativeSelect elevation={value} defaultValue="devnet" className="w-48" > <NativeSelectOption value="devnet"> Solana devnet </NativeSelectOption> <NativeSelectOption value="mainnet">Mainnet</NativeSelectOption> </NativeSelect> </div> ))} </div> );}<script setup lang="ts">import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }];</script>
<template> <div class="flex flex-col gap-5"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <NativeSelect model-value="devnet" :elevation="level.value" class="w-48"> <NativeSelectOption value="devnet">Solana devnet</NativeSelectOption> <NativeSelectOption value="mainnet">Mainnet</NativeSelectOption> </NativeSelect> </div> </div></template><script lang="ts"> import { NativeSelect, NativeSelectOption } from "$lib/components/ui/native-select";
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<div class="flex flex-col gap-5"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <NativeSelect elevation={level.value} class="w-48"> <NativeSelectOption value="devnet">Solana devnet</NativeSelectOption> <NativeSelectOption value="mainnet">Mainnet</NativeSelectOption> </NativeSelect> </div> {/each}</div>| Prop | Type | Default |
|---|---|---|
| size | sm | default |
default |
elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
✦ Edmi additions
Section titled “✦ Edmi additions”NativeSelectOptionandNativeSelectOptGroupwrap the native elements.