Select
Custom select in a popover with groups, labels, separators and a check on the chosen item.
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue,} from "@/components/ui/select";
const tokens = [ { value: "nvdax", label: "NVDAx" }, { value: "msftx", label: "MSFTx" }, { value: "aaplx", label: "AAPLx" }, { value: "anthrop", label: "ANTHRP-pre" }, { value: "openai", label: "OPENAI-pre" },];
export default function Demo() { return ( <Select defaultValue="nvdax" items={tokens}> <SelectTrigger className="w-52"> <SelectValue placeholder="Select a token" /> </SelectTrigger> <SelectContent alignItemWithTrigger={false}> <SelectGroup> <SelectLabel>US megacaps</SelectLabel> <SelectItem value="nvdax">NVDAx</SelectItem> <SelectItem value="msftx">MSFTx</SelectItem> <SelectItem value="aaplx">AAPLx</SelectItem> </SelectGroup> <SelectSeparator /> <SelectGroup> <SelectLabel>Pre-IPO</SelectLabel> <SelectItem value="anthrop">ANTHRP-pre</SelectItem> <SelectItem value="openai" disabled> OPENAI-pre </SelectItem> </SelectGroup> </SelectContent> </Select> );}<script setup lang="ts">import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue,} from "@/components/ui/select";</script>
<template> <Select default-value="nvdax"> <SelectTrigger class="w-52"> <SelectValue placeholder="Select a token" /> </SelectTrigger> <SelectContent> <SelectGroup> <SelectLabel>US megacaps</SelectLabel> <SelectItem value="nvdax">NVDAx</SelectItem> <SelectItem value="msftx">MSFTx</SelectItem> <SelectItem value="aaplx">AAPLx</SelectItem> </SelectGroup> <SelectSeparator /> <SelectGroup> <SelectLabel>Pre-IPO</SelectLabel> <SelectItem value="anthrop">ANTHRP-pre</SelectItem> <SelectItem value="openai" disabled>OPENAI-pre</SelectItem> </SelectGroup> </SelectContent> </Select></template><script lang="ts"> import { Select, SelectContent, SelectGroup, SelectGroupHeading, SelectItem, SelectSeparator, SelectTrigger, } from "$lib/components/ui/select";
const tokens: Record<string, string> = { nvdax: "NVDAx", msftx: "MSFTx", aaplx: "AAPLx", anthrop: "ANTHRP-pre", openai: "OPENAI-pre", };
let value = $state("nvdax");</script>
<Select type="single" bind:value> <SelectTrigger class="w-52"> {value ? tokens[value] : "Select a token"} </SelectTrigger> <SelectContent> <SelectGroup> <SelectGroupHeading>US megacaps</SelectGroupHeading> <SelectItem value="nvdax" label="NVDAx" /> <SelectItem value="msftx" label="MSFTx" /> <SelectItem value="aaplx" label="AAPLx" /> </SelectGroup> <SelectSeparator /> <SelectGroup> <SelectGroupHeading>Pre-IPO</SelectGroupHeading> <SelectItem value="anthrop" label="ANTHRP-pre" /> <SelectItem value="openai" label="OPENAI-pre" disabled /> </SelectGroup> </SelectContent></Select>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/selectpnpm dlx shadcn@latest add @edmi-ui/selectyarn dlx shadcn@latest add @edmi-ui/selectbunx shadcn@latest add @edmi-ui/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 @base-ui/reactTerminal window pnpm add @base-ui/reactTerminal window yarn add @base-ui/reactTerminal window bun add @base-ui/reactAdd 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/selectpnpm dlx shadcn-vue@latest add @edmi-ui/selectyarn dlx shadcn-vue@latest add @edmi-ui/selectbunx shadcn-vue@latest add @edmi-ui/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 reka-ui @vueuse/coreTerminal window pnpm add reka-ui @vueuse/coreTerminal window yarn add reka-ui @vueuse/coreTerminal window bun add reka-ui @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/select.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/select.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/select.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/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:
separator,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue,} from "@/components/ui/select";
<Select defaultValue="nvdax"> <SelectTrigger className="w-52"> <SelectValue placeholder="Select a token" /> </SelectTrigger> <SelectContent alignItemWithTrigger={false}> <SelectGroup> <SelectLabel>US megacaps</SelectLabel> <SelectItem value="nvdax">NVDAx</SelectItem> <SelectItem value="msftx">MSFTx</SelectItem> <SelectItem value="aaplx">AAPLx</SelectItem> </SelectGroup> <SelectSeparator /> <SelectGroup> <SelectLabel>Pre-IPO</SelectLabel> <SelectItem value="anthrop">ANTHRP-pre</SelectItem> <SelectItem value="openai" disabled> OPENAI-pre </SelectItem> </SelectGroup> </SelectContent></Select><script setup lang="ts">import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue,} from "@/components/ui/select";</script>
<template> <Select default-value="nvdax"> <SelectTrigger class="w-52"> <SelectValue placeholder="Select a token" /> </SelectTrigger> <SelectContent> <SelectGroup> <SelectLabel>US megacaps</SelectLabel> <SelectItem value="nvdax">NVDAx</SelectItem> <SelectItem value="msftx">MSFTx</SelectItem> <SelectItem value="aaplx">AAPLx</SelectItem> </SelectGroup> <SelectSeparator /> <SelectGroup> <SelectLabel>Pre-IPO</SelectLabel> <SelectItem value="anthrop">ANTHRP-pre</SelectItem> <SelectItem value="openai" disabled>OPENAI-pre</SelectItem> </SelectGroup> </SelectContent> </Select></template><script lang="ts"> import { Select, SelectContent, SelectGroup, SelectGroupHeading, SelectItem, SelectSeparator, SelectTrigger, } from "$lib/components/ui/select";</script>
<Select type="single" bind:value> <SelectTrigger class="w-52"> </SelectTrigger> <SelectContent> <SelectGroup> <SelectGroupHeading>US megacaps</SelectGroupHeading> <SelectItem value="nvdax" label="NVDAx" /> <SelectItem value="msftx" label="MSFTx" /> <SelectItem value="aaplx" label="AAPLx" /> </SelectGroup> <SelectSeparator /> <SelectGroup> <SelectGroupHeading>Pre-IPO</SelectGroupHeading> <SelectItem value="anthrop" label="ANTHRP-pre" /> <SelectItem value="openai" label="OPENAI-pre" disabled /> </SelectGroup> </SelectContent></Select>Component source
Section titled “Component source”"use client";
import { Select as SelectPrimitive } from "@base-ui/react/select";import { cn } from "cn";import type * as React from "react";import { CaretDownIcon, CheckIcon, CaretUpIcon } 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 data-[popup-open]:bg-card 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",};
// ✦ depth (v4): overlay role for the menu popup. Natural level is floating in layered mode; flat otherwise.const overlayElevation = { sunken: "", flat: "", raised: "border-transparent shadow-raised", floating: "border-transparent shadow-floating",};
const Select = SelectPrimitive.Root;
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { return ( <SelectPrimitive.Group data-slot="select-group" className={cn("scroll-my-1", className)} {...props} /> );}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { return ( <SelectPrimitive.Value data-slot="select-value" className={cn("flex flex-1 text-left", className)} {...props} /> );}
function SelectTrigger({ className, size = "default", elevation, children, ...props}: SelectPrimitive.Trigger.Props & { size?: "sm" | "default"; /** ✦ depth for the trigger (the popup stays floating): sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation;}) { const level = useElevation(elevation, "field"); return ( <SelectPrimitive.Trigger data-slot="select-trigger" data-size={size} className={cn( "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-card pr-2.5 pl-3 text-sm whitespace-nowrap text-foreground outline-none select-none focus-visible:border-ring focus-visible:shadow-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-ring-error data-[popup-open]:border-ring data-[popup-open]:shadow-ring data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 data-[size=sm]:rounded-[7px] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", fieldElevation[level], className, )} {...props} > {children} <SelectPrimitive.Icon render={ <CaretDownIcon className="pointer-events-none size-4 text-muted-foreground" /> } /> </SelectPrimitive.Trigger> );}
function SelectContent({ className, children, side = "bottom", sideOffset = 4, align = "center", alignOffset = 0, alignItemWithTrigger = true, elevation, ...props}: SelectPrimitive.Popup.Props & Pick< SelectPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" > & { /** ✦ depth of the menu popup: flat 0, raised +1 (bevel), floating +2 (bevel + drop). */ elevation?: Elevation; }) { const level = useElevation(elevation, "overlay"); return ( <SelectPrimitive.Portal> <SelectPrimitive.Positioner side={side} sideOffset={sideOffset} align={align} alignOffset={alignOffset} alignItemWithTrigger={alignItemWithTrigger} className="isolate z-50" > <SelectPrimitive.Popup data-slot="select-content" data-align-trigger={alignItemWithTrigger} className={cn( "relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border border-border bg-popover p-1.5 text-popover-foreground duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[open]:animate-in data-[open]:fade-in-0 data-[open]:zoom-in-95 data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95", overlayElevation[level], className, )} {...props} > <SelectScrollUpButton /> <SelectPrimitive.List>{children}</SelectPrimitive.List> <SelectScrollDownButton /> </SelectPrimitive.Popup> </SelectPrimitive.Positioner> </SelectPrimitive.Portal> );}
function SelectLabel({ className, ...props}: SelectPrimitive.GroupLabel.Props) { return ( <SelectPrimitive.GroupLabel data-slot="select-label" className={cn( "px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground", className, )} {...props} /> );}
function SelectItem({ className, children, ...props}: SelectPrimitive.Item.Props) { return ( <SelectPrimitive.Item data-slot="select-item" className={cn( "relative flex h-8 w-full cursor-default items-center gap-2.5 rounded-[7px] pr-8 pl-2 text-[13.5px] outline-hidden select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className, )} {...props} > <SelectPrimitive.ItemText className="flex flex-1 shrink-0 items-center gap-2 whitespace-nowrap"> {children} </SelectPrimitive.ItemText> <SelectPrimitive.ItemIndicator render={ <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" /> } > <CheckIcon className="pointer-events-none" /> </SelectPrimitive.ItemIndicator> </SelectPrimitive.Item> );}
function SelectSeparator({ className, ...props}: SelectPrimitive.Separator.Props) { return ( <SelectPrimitive.Separator data-slot="select-separator" className={cn( "pointer-events-none -mx-1.5 my-1 h-px bg-border", className, )} {...props} /> );}
function SelectScrollUpButton({ className, ...props}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) { return ( <SelectPrimitive.ScrollUpArrow data-slot="select-scroll-up-button" className={cn( "top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} > <CaretUpIcon /> </SelectPrimitive.ScrollUpArrow> );}
function SelectScrollDownButton({ className, ...props}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) { return ( <SelectPrimitive.ScrollDownArrow data-slot="select-scroll-down-button" className={cn( "bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} > <CaretDownIcon /> </SelectPrimitive.ScrollDownArrow> );}
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue,};<script setup lang="ts">import type { SelectRootEmits, SelectRootProps } from "reka-ui"import { SelectRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<SelectRootProps>()const emits = defineEmits<SelectRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)</script>
<template> <SelectRoot v-slot="slotProps" data-slot="select" v-bind="forwarded" > <slot v-bind="slotProps" /> </SelectRoot></template><script setup lang="ts">import type { SelectContentEmits, SelectContentProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { SelectContent, SelectPortal, SelectViewport, useForwardPropsEmits,} from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'import { SelectScrollDownButton, SelectScrollUpButton } from '.'
defineOptions({ inheritAttrs: false,})
const props = withDefaults( defineProps<SelectContentProps & { class?: HTMLAttributes['class'], elevation?: Elevation }>(), { elevation: undefined, position: 'popper', sideOffset: 4, },)const emits = defineEmits<SelectContentEmits>()
// ✦ depth (v4): overlay role. Natural level is floating in layered mode; flat otherwise.const overlayElevation = { sunken: '', flat: '', raised: 'border-transparent shadow-raised', floating: 'border-transparent shadow-floating',}const level = useElevation(() => props.elevation, 'overlay')
const delegatedProps = reactiveOmit(props, 'class', 'elevation')
const forwarded = useForwardPropsEmits(delegatedProps, emits)</script>
<template> <SelectPortal> <SelectContent data-slot="select-content" v-bind="{ ...$attrs, ...forwarded }" :class="cn( 'relative isolate z-50 max-h-(--reka-select-content-available-height) min-w-36 origin-(--reka-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border border-border bg-popover text-popover-foreground duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95', overlayElevation[level], props.class, ) " > <SelectScrollUpButton /> <SelectViewport :class="cn('p-1.5', position === 'popper' && 'w-full min-w-(--reka-select-trigger-width) scroll-my-1')"> <slot /> </SelectViewport> <SelectScrollDownButton /> </SelectContent> </SelectPortal></template><script setup lang="ts">import type { SelectGroupProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { reactiveOmit } from "@vueuse/core"import { SelectGroup } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<SelectGroupProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")</script>
<template> <SelectGroup data-slot="select-group" v-bind="delegatedProps" :class="cn('scroll-my-1', props.class)" > <slot /> </SelectGroup></template><script setup lang="ts">import type { SelectItemProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { Check } from '@lucide/vue'import { reactiveOmit } from '@vueuse/core'import { SelectItem, SelectItemIndicator, SelectItemText, useForwardProps,} from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<SelectItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)</script>
<template> <SelectItem data-slot="select-item" v-bind="forwardedProps" :class=" cn( `relative flex h-8 w-full cursor-default items-center gap-2.5 rounded-[7px] pr-8 pl-2 text-[13.5px] outline-hidden select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2`, props.class, ) " > <SelectItemText class="flex flex-1 shrink-0 items-center gap-2 whitespace-nowrap"> <slot /> </SelectItemText>
<span class="pointer-events-none absolute right-2 flex size-4 items-center justify-center"> <SelectItemIndicator> <slot name="indicator-icon"> <Check class="pointer-events-none size-4" /> </slot> </SelectItemIndicator> </span> </SelectItem></template><script setup lang="ts">import type { SelectItemTextProps } from "reka-ui"import { SelectItemText } from "reka-ui"
const props = defineProps<SelectItemTextProps>()</script>
<template> <SelectItemText data-slot="select-item-text" v-bind="props" > <slot /> </SelectItemText></template><script setup lang="ts">import type { SelectLabelProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { SelectLabel } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes['class'] }>()</script>
<template> <SelectLabel data-slot="select-label" :class="cn('px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground', props.class)" > <slot /> </SelectLabel></template><script setup lang="ts">import type { SelectScrollDownButtonProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { ChevronDown } from '@lucide/vue'import { reactiveOmit } from '@vueuse/core'import { SelectScrollDownButton, useForwardProps } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)</script>
<template> <SelectScrollDownButton data-slot="select-scroll-down-button" v-bind="forwardedProps" :class="cn('z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*=\'size-\'])]:size-4', props.class)" > <slot> <ChevronDown /> </slot> </SelectScrollDownButton></template><script setup lang="ts">import type { SelectScrollUpButtonProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { ChevronUp } from '@lucide/vue'import { reactiveOmit } from '@vueuse/core'import { SelectScrollUpButton, useForwardProps } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)</script>
<template> <SelectScrollUpButton data-slot="select-scroll-up-button" v-bind="forwardedProps" :class="cn('z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*=\'size-\'])]:size-4', props.class)" > <slot> <ChevronUp /> </slot> </SelectScrollUpButton></template><script setup lang="ts">import type { SelectSeparatorProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { SelectSeparator } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')</script>
<template> <SelectSeparator data-slot="select-separator" v-bind="delegatedProps" :class="cn('pointer-events-none -mx-1.5 my-1 h-px bg-border', props.class)" /></template><script setup lang="ts">import type { SelectTriggerProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { ChevronDown } from '@lucide/vue'import { reactiveOmit } from '@vueuse/core'import { SelectIcon, SelectTrigger, useForwardProps } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'
const props = withDefaults( defineProps<SelectTriggerProps & { class?: HTMLAttributes['class'], size?: 'sm' | 'default', elevation?: Elevation }>(), { size: 'default', elevation: undefined },)
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 data-[state=open]:bg-card 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 data-[state=open]:border-ring data-[state=open]: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 data-[state=open]:border-ring data-[state=open]:shadow-ring',}
const level = useElevation(() => props.elevation, 'field')const forwardedProps = useForwardProps(delegatedProps)</script>
<template> <SelectTrigger data-slot="select-trigger" :data-size="size" v-bind="forwardedProps" :class="cn( `flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-card pr-2.5 pl-3 text-sm whitespace-nowrap text-foreground outline-none select-none focus-visible:border-ring focus-visible:shadow-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-ring-error data-[state=open]:border-ring data-[state=open]:shadow-ring data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 data-[size=sm]:rounded-[7px] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4`, fieldElevation[level], props.class, )" > <slot /> <SelectIcon as-child> <ChevronDown class="pointer-events-none size-4 text-muted-foreground" /> </SelectIcon> </SelectTrigger></template><script setup lang="ts">import type { SelectValueProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { reactiveOmit } from "@vueuse/core"import { SelectValue } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<SelectValueProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")</script>
<template> <SelectValue data-slot="select-value" v-bind="delegatedProps" :class="cn('flex flex-1 text-left', props.class)" > <slot /> </SelectValue></template>export { default as Select } from "./Select.vue";export { default as SelectContent } from "./SelectContent.vue";export { default as SelectGroup } from "./SelectGroup.vue";export { default as SelectItem } from "./SelectItem.vue";export { default as SelectItemText } from "./SelectItemText.vue";export { default as SelectLabel } from "./SelectLabel.vue";export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue";export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue";export { default as SelectSeparator } from "./SelectSeparator.vue";export { default as SelectTrigger } from "./SelectTrigger.vue";export { default as SelectValue } from "./SelectValue.vue";<script lang="ts"> import { Select as SelectPrimitive } from "bits-ui"; import { cn, type WithoutChild } from "$lib/utils.js"; import type { WithoutChildrenOrChild } from "$lib/utils.js"; import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js"; import SelectPortal from "./select-portal.svelte"; import SelectScrollDownButton from "./select-scroll-down-button.svelte"; import SelectScrollUpButton from "./select-scroll-up-button.svelte"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, sideOffset = 4, portalProps, children, preventScroll = true, elevation = "auto", ...restProps }: WithoutChild<SelectPrimitive.ContentProps> & { /** ✦ depth: flat 0, raised +1 (bevel), floating +2 (bevel + drop). */ elevation?: Elevation; portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>; } = $props();
// ✦ depth (v4): overlay role. Natural level is floating in layered mode; flat otherwise. const overlayElevation = { sunken: "", flat: "", raised: "border-transparent shadow-raised", floating: "border-transparent shadow-floating", }; const level = useElevation(() => elevation, "overlay");</script>
<SelectPortal {...portalProps}> <SelectPrimitive.Content bind:ref {sideOffset} {preventScroll} data-slot="select-content" class={cn( "relative isolate z-50 max-h-(--bits-select-content-available-height) min-w-36 origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border border-border bg-popover text-popover-foreground duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", overlayElevation[level.current], className )} {...restProps} > <SelectScrollUpButton /> <SelectPrimitive.Viewport class={cn( "w-full min-w-(--bits-select-anchor-width) scroll-my-1 p-1.5" )} > {@render children?.()} </SelectPrimitive.Viewport> <SelectScrollDownButton /> </SelectPrimitive.Content></SelectPortal><script lang="ts"> import { Select as SelectPrimitive } from "bits-ui"; import { cn } from "$lib/utils.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, children, ...restProps }: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();</script>
<SelectPrimitive.GroupHeading bind:ref data-slot="select-group-heading" class={cn("px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground", className)} {...restProps}> {@render children?.()}</SelectPrimitive.GroupHeading><script lang="ts"> import { Select as SelectPrimitive } from "bits-ui"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: SelectPrimitive.GroupProps = $props();</script>
<SelectPrimitive.Group bind:ref data-slot="select-group" class={cn("scroll-my-1", className)} {...restProps}/><script lang="ts"> import { Select as SelectPrimitive } from "bits-ui"; import CheckIcon from "phosphor-svelte/lib/Check"; import { cn, type WithoutChild } from "$lib/utils.js";
let { ref = $bindable(null), class: className, value, label, children: childrenProp, ...restProps }: WithoutChild<SelectPrimitive.ItemProps> = $props();</script>
<SelectPrimitive.Item bind:ref {value} {label} data-slot="select-item" class={cn( "relative flex h-8 w-full cursor-default items-center gap-2.5 rounded-[7px] pr-8 pl-2 text-[13.5px] outline-hidden select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className )} {...restProps}> {#snippet children({ selected, highlighted })} <span class="pointer-events-none absolute right-2 flex size-4 items-center justify-center"> {#if selected} <CheckIcon class="pointer-events-none" /> {/if} </span> <span class="flex flex-1 shrink-0 items-center gap-2 whitespace-nowrap"> {#if childrenProp} {@render childrenProp({ selected, highlighted })} {:else} {label || value} {/if} </span> {/snippet}</SelectPrimitive.Item><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();</script>
<div bind:this={ref} data-slot="select-label" class={cn("px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { Select as SelectPrimitive } from "bits-ui";
let { ...restProps }: SelectPrimitive.PortalProps = $props();</script>
<SelectPrimitive.Portal {...restProps} /><script lang="ts"> import { Select as SelectPrimitive } from "bits-ui"; import CaretDownIcon from "phosphor-svelte/lib/CaretDown"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();</script>
<SelectPrimitive.ScrollDownButton bind:ref data-slot="select-scroll-down-button" class={cn("z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", className)} {...restProps}> <CaretDownIcon /></SelectPrimitive.ScrollDownButton><script lang="ts"> import { Select as SelectPrimitive } from "bits-ui"; import CaretUpIcon from "phosphor-svelte/lib/CaretUp"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();</script>
<SelectPrimitive.ScrollUpButton bind:ref data-slot="select-scroll-up-button" class={cn("z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", className)} {...restProps}> <CaretUpIcon /></SelectPrimitive.ScrollUpButton><script lang="ts"> import { Separator } from "$lib/registry/ui/separator/index.js"; import { cn } from "$lib/utils.js"; import type { Separator as SeparatorPrimitive } from "bits-ui";
let { ref = $bindable(null), class: className, ...restProps }: SeparatorPrimitive.RootProps = $props();</script>
<Separator bind:ref data-slot="select-separator" class={cn("pointer-events-none -mx-1.5 my-1 h-px bg-border", className)} {...restProps}/><script lang="ts"> import { Select as SelectPrimitive } from "bits-ui"; import CaretDownIcon from "phosphor-svelte/lib/CaretDown"; import { cn, type WithoutChild } from "$lib/utils.js"; import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
let { ref = $bindable(null), class: className, children, size = "default", elevation = "auto", ...restProps }: WithoutChild<SelectPrimitive.TriggerProps> & { size?: "sm" | "default"; /** ✦ depth for the trigger (popup stays floating): sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation; } = $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 data-[state=open]:bg-card 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 data-[state=open]:border-ring data-[state=open]: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 data-[state=open]:border-ring data-[state=open]:shadow-ring", };
const level = useElevation(() => elevation, "field");</script>
<SelectPrimitive.Trigger bind:ref data-slot="select-trigger" data-size={size} class={cn( "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-card pr-2.5 pl-3 text-sm whitespace-nowrap text-foreground outline-none select-none focus-visible:border-ring focus-visible:shadow-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-ring-error data-[state=open]:border-ring data-[state=open]:shadow-ring data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 data-[size=sm]:rounded-[7px] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", fieldElevation[level.current], className )} {...restProps}> {@render children?.()} <CaretDownIcon class="pointer-events-none size-4 text-muted-foreground" /></SelectPrimitive.Trigger><script lang="ts"> import { Select as SelectPrimitive } from "bits-ui"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: SelectPrimitive.ValueProps = $props();</script>
<SelectPrimitive.Value bind:ref data-slot="select-value" class={cn("flex flex-1 text-left", className)} {...restProps}/><script lang="ts"> import { Select as SelectPrimitive } from "bits-ui";
let { open = $bindable(false), value = $bindable(), ...restProps }: SelectPrimitive.RootProps = $props();</script>
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />import Root from "./select.svelte";import Content from "./select-content.svelte";import Group from "./select-group.svelte";import GroupHeading from "./select-group-heading.svelte";import Item from "./select-item.svelte";import Label from "./select-label.svelte";import Portal from "./select-portal.svelte";import ScrollDownButton from "./select-scroll-down-button.svelte";import ScrollUpButton from "./select-scroll-up-button.svelte";import Separator from "./select-separator.svelte";import Trigger from "./select-trigger.svelte";import Value from "./select-value.svelte";
export { Content, Content as SelectContent, Group, Group as SelectGroup, GroupHeading, GroupHeading as SelectGroupHeading, Item, Item as SelectItem, Label, Label as SelectLabel, Portal, Portal as SelectPortal, Root, // Root as Select, ScrollDownButton, ScrollDownButton as SelectScrollDownButton, ScrollUpButton, ScrollUpButton as SelectScrollUpButton, Separator, Separator as SelectSeparator, Trigger, Trigger as SelectTrigger, Value, Value as SelectValue,};Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
elevation on SelectTrigger takes sunken -1, flat, raised +1 or floating +2. The popup (SelectContent) takes its own elevation (flat by default; floating is its natural level in layered mode).
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select";
const tokens = [ { value: "nvdax", label: "NVDAx" }, { value: "msftx", label: "MSFTx" },];
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> <Select defaultValue="nvdax" items={tokens}> <SelectTrigger elevation={value} className="w-48"> <SelectValue placeholder="Select a token" /> </SelectTrigger> <SelectContent alignItemWithTrigger={false}> <SelectItem value="nvdax">NVDAx</SelectItem> <SelectItem value="msftx">MSFTx</SelectItem> </SelectContent> </Select> </div> ))} </div> );}<script setup lang="ts">import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/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> <Select default-value="nvdax"> <SelectTrigger :elevation="level.value" class="w-48"><SelectValue /></SelectTrigger> <SelectContent> <SelectItem value="nvdax">NVDAx</SelectItem> <SelectItem value="msftx">MSFTx</SelectItem> </SelectContent> </Select> </div> </div></template><script lang="ts"> import { Select, SelectContent, SelectItem, SelectTrigger } from "$lib/components/ui/select";
let token = $state("nvdax");
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> <Select type="single" bind:value={token}> <SelectTrigger elevation={level.value} class="w-48">{token}</SelectTrigger> <SelectContent> <SelectItem value="nvdax" label="nvdax" /> <SelectItem value="msftx" label="msftx" /> </SelectContent> </Select> </div> {/each}</div>| Prop | Type | Default |
|---|---|---|
| SelectTrigger size | sm | default |
default |
| SelectContent alignItemWithTrigger | boolean |
true |
| value / defaultValue | string |
none |
| items (React) | { value, label }[] |
none |
SelectTrigger elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
SelectContent elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
✦ Edmi additions
Section titled “✦ Edmi additions”- Flat popover surface;
elevationon the trigger and onSelectContentare independent; highlighted item uses the accent fill.
Vue / Svelte differences
Section titled “Vue / Svelte differences”- React: Base UI shows the raw
valueinSelectValueunless the root getsitems={[{ value, label }]}(orSelectValuea render function), so passitemswith the labels. Vue and Svelte show the selected item text automatically. - Svelte: the root requires
type="single"(or"multiple"); React and Vue infer single selection.