
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
selectuiDropdown select primitive — single-select with optional groups, descriptions per item, headless popover mode (reka-ui), and zero-JS styled NativeSelect for lightweight/mobile use.
Also available for React ->Select is the headless primitive for flat options. Advance Select is the searchable combobox. Cascade and Tree selectors are designed specifically for hierarchical and nested datasets.
| Component | Role | Use when |
|---|---|---|
| Selectcurrent Short, flat option lists (< 20 items) where typing to filter is unnecessary. | Primitive Dropdown | |
| AdvanceSelect Large datasets requiring fuzzy search, multi-select tag pills, or remote option filtering. | Searchable Combobox | |
| CascadeSelect Sequential multi-level drilldowns (e.g. Country > State > City, Category > Subcategory). | Hierarchical Picker | |
| TreeSelect Deeply nested taxonomy structures, folder trees, or parent-child permission nodes. | Tree Picker |
$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/select.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/select.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/select.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/select.jsonnpx shadcn-vue@latest add @uipkge/selectInstalls to:app/components/ui/select/Type aliases from this item's source — use them to shape the data you pass in.
NativeSelectOptioninterface NativeSelectOption {
label: string
value: string | number
disabled?: boolean
}<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-uipkge 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 '@/lib/utils'
import { SelectScrollDownButton, SelectScrollUpButton } from '.'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SelectContentProps & { class?: HTMLAttributes['class'] }>(), {
position: 'popper',
})
const emits = defineEmits<SelectContentEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SelectPortal>
<SelectContent
data-uipkge
data-slot="select-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-popover text-popover-foreground motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:ease-emphasized motion-safe:data-[state=open]:blur-in-2 motion-safe:data-[state=closed]:blur-out-2 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-32 overflow-x-hidden overflow-y-auto rounded-md border shadow-md motion-safe:data-[state=closed]:duration-[var(--dur-exit)] motion-safe:data-[state=open]:duration-200',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
props.class,
)
"
>
<SelectScrollUpButton />
<SelectViewport
:class="
cn(
'p-1',
position === 'popper' &&
'h-[var(--reka-select-trigger-height)] w-full min-w-[var(--reka-select-trigger-width)] scroll-my-1',
)
"
>
<slot />
</SelectViewport>
<SelectScrollDownButton />
</SelectContent>
</SelectPortal>
</template>
<script setup lang="ts">
import type { SelectGroupProps } from 'reka-ui'
import { SelectGroup } from 'reka-ui'
const props = defineProps<SelectGroupProps>()
</script>
<template>
<SelectGroup data-uipkge data-slot="select-group" v-bind="props">
<slot />
</SelectGroup>
</template>
<script setup lang="ts">
import type { SelectItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { Check } from 'lucide-vue-next'
import { SelectItem, SelectItemIndicator, SelectItemText, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<SelectItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectItem
data-uipkge
data-slot="select-item"
v-bind="forwardedProps"
:class="
cn(
'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_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,
)
"
>
<span class="absolute right-2 flex size-3.5 items-center justify-center">
<SelectItemIndicator>
<slot name="indicator-icon">
<Check class="size-4" aria-hidden="true" />
</slot>
</SelectItemIndicator>
</span>
<SelectItemText>
<slot />
</SelectItemText>
</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-uipkge 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 '@/lib/utils'
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes['class'] }>()
</script>
<template>
<SelectLabel data-uipkge data-slot="select-label" :class="cn('text-muted-foreground px-2 py-2 text-xs', props.class)">
<slot />
</SelectLabel>
</template>
<script setup lang="ts">
import type { SelectScrollDownButtonProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ChevronDown } from 'lucide-vue-next'
import { SelectScrollDownButton, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectScrollDownButton
data-uipkge
data-slot="select-scroll-down-button"
v-bind="forwardedProps"
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
>
<slot>
<ChevronDown class="size-4" aria-hidden="true" />
</slot>
</SelectScrollDownButton>
</template>
<script setup lang="ts">
import type { SelectScrollUpButtonProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ChevronUp } from 'lucide-vue-next'
import { SelectScrollUpButton, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<SelectScrollUpButton
data-uipkge
data-slot="select-scroll-up-button"
v-bind="forwardedProps"
:class="cn('flex cursor-default items-center justify-center py-1', props.class)"
>
<slot>
<ChevronUp class="size-4" aria-hidden="true" />
</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 '@/lib/utils'
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<SelectSeparator
data-uipkge
data-slot="select-separator"
v-bind="delegatedProps"
:class="cn('bg-border pointer-events-none -mx-1 my-1 h-px', props.class)"
/>
</template>
<script setup lang="ts">
import type { SelectTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ChevronDown, Loader } from 'lucide-vue-next'
import { SelectIcon, SelectTrigger, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<
SelectTriggerProps & {
class?: HTMLAttributes['class']
size?: 'sm' | 'default' | 'lg'
state?: 'default' | 'error' | 'success'
loading?: boolean
}
>(),
{ size: 'default', state: 'default', loading: false },
)
const delegatedProps = reactiveOmit(props, 'class', 'size', 'state', 'loading')
const forwardedProps = useForwardProps(delegatedProps)
const sizeClasses = {
sm: 'h-8 text-sm px-2.5 py-1.5',
default: 'h-9 text-sm px-3 py-2',
lg: 'h-11 text-base px-4 py-2.5',
}
const stateClasses = {
default: 'border-input dark:hover:bg-input/50',
error:
'border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
success: 'border-success focus-visible:border-success',
}
</script>
<template>
<SelectTrigger
data-uipkge
data-slot="select-trigger"
:data-size="size"
:data-state-value="state"
v-bind="forwardedProps"
:disabled="disabled || loading"
:aria-busy="loading"
:class="
cn(
'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex w-full items-center justify-between gap-2 rounded-md border bg-transparent text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
sizeClasses[size],
stateClasses[state],
props.class,
)
"
>
<slot />
<SelectIcon v-if="!loading" as-child>
<ChevronDown class="size-4 opacity-50" aria-hidden="true" />
</SelectIcon>
<Loader v-else class="size-4 animate-spin opacity-50" aria-hidden="true" />
</SelectTrigger>
</template>
<script setup lang="ts">
import type { SelectValueProps } from 'reka-ui'
import { SelectValue } from 'reka-ui'
const props = defineProps<SelectValueProps>()
</script>
<template>
<SelectValue data-uipkge data-slot="select-value" v-bind="props">
<slot />
</SelectValue>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { computed, useAttrs } from 'vue'
import { useVModel } from '@vueuse/core'
import { ChevronDown } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
defineOptions({
inheritAttrs: false,
})
export interface NativeSelectOption {
label: string
value: string | number
disabled?: boolean
}
interface Props {
modelValue?: string | number
defaultValue?: string | number
options?: (NativeSelectOption | string)[]
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
class?: HTMLAttributes['class']
selectClass?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
size: 'md',
})
const emits = defineEmits<{
(e: 'update:modelValue', val: string | number): void
}>()
const attrs = useAttrs()
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
defaultValue: props.defaultValue,
})
const sizeClasses: Record<string, string> = {
sm: 'h-8 text-xs pl-2.5 pr-8',
md: 'h-9 text-sm pl-3 pr-9',
lg: 'h-11 text-base pl-4 pr-10',
}
const iconSizes: Record<string, string> = {
sm: 'size-3.5 right-2.5',
md: 'size-4 right-3',
lg: 'size-5 right-3.5',
}
const normalizedOptions = computed(() => {
if (!props.options) return []
return props.options.map((opt) => {
if (typeof opt === 'string') {
return { label: opt, value: opt, disabled: false }
}
return opt
})
})
</script>
<template>
<div
data-uipkge
data-slot="native-select-wrapper"
:class="cn('relative inline-flex w-full items-center', props.class)"
>
<select
data-slot="native-select"
:disabled="disabled"
:value="modelValue"
:class="
cn(
'border-input bg-background w-full appearance-none rounded-md border shadow-xs transition-[color,box-shadow]',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:outline-none',
'disabled:bg-muted/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
sizeClasses[size],
props.selectClass,
)
"
v-bind="attrs"
@change="modelValue = ($event.target as HTMLSelectElement).value"
>
<template v-if="normalizedOptions.length > 0">
<option v-for="opt in normalizedOptions" :key="String(opt.value)" :value="opt.value" :disabled="opt.disabled">
{{ opt.label }}
</option>
</template>
<slot v-else />
</select>
<ChevronDown
data-slot="native-select-icon"
aria-hidden="true"
:class="
cn(
'text-muted-foreground pointer-events-none absolute transition-opacity',
disabled && 'opacity-50',
iconSizes[size],
)
"
/>
</div>
</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'
export { default as NativeSelect, type NativeSelectOption } from './NativeSelect.vue'
export { type SelectOption, readKey } from './option-types'
/**
* Shared option shape for `AdvanceSelect` and any future options-driven select.
* Components accept this canonical shape OR any object shape if `value-key` /
* `label-key` are provided.
*/
export interface SelectOption<V = string> {
label: string
value: V
disabled?: boolean
/** Items sharing this key render under one heading. */
group?: string
}
/**
* Resolve `option[key]` with a default fallback. Used by AdvanceSelect so every
* accessor obeys `value-key` / `label-key`.
*/
export function readKey<T>(option: T, key: string, fallback?: unknown): unknown {
if (option == null || typeof option !== 'object') return fallback
const v = (option as Record<string, unknown>)[key]
return v === undefined ? fallback : v
}
Raw manifest:https://uipkge.dev/r/vue/select.json