Select
Displays a list of options for the user to pick from—triggered by a button.
Preview
<script setup lang="ts">
import {
Select,
SelectTrigger,
SelectContent,
SelectOption,
SelectValue,
SelectLabel,
} from "~/components/ui/select";
</script>
<template>
<Select>
<SelectLabel>Fruit</SelectLabel>
<SelectTrigger class="w-[180px]">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectOption value="apple" label="Apple" />
<SelectOption value="banana" label="Banana" />
<SelectOption value="blueberry" label="Blueberry" />
<SelectOption value="grapes" label="Grapes" />
<SelectOption value="pineapple" label="Pineapple" />
</SelectContent>
</Select>
</template>
Installation
Copy and paste this into your project
// ~/components/ui/select.tsx
import {
Select as SelectPrimitive,
SelectContent as SelectContentPrimitive,
SelectOption as SelectOptionPrimitive,
SelectPositioner,
SelectTrigger as SelectTriggerPrimitive,
SelectLabel as SelectLabelPrimitive,
SelectProps,
} from "@ark-ui/vue";
import { cn, ExtendProps } from "~/lib/utils";
import {
defineComponent,
onMounted,
ref,
computed,
Teleport,
InjectionKey,
ComputedRef,
provide,
inject,
h,
PropType,
} from "vue";
import { CheckIcon, ChevronDown } from "lucide-vue-next";
import { labelClasses } from "~/components/ui/label";
const Check = defineComponent({
setup() {
return () =>
h(CheckIcon, {
class: "w-4 h-4",
});
},
});
const Chevron = defineComponent({
setup() {
return () =>
h(ChevronDown, {
class: "w-4 h-4 ml-auto",
});
},
});
const selectedOptionKey = Symbol() as InjectionKey<
ComputedRef<
| {
value: string;
label: string;
}
| undefined
>
>;
const SelectContext = defineComponent({
props: {
selectedOption: {
type: Object as PropType<{
value: string;
label: string;
}>,
},
},
setup(props, { slots }) {
provide(
selectedOptionKey,
computed(() => props.selectedOption)
);
return () => slots.default?.();
},
});
const Select = defineComponent({
props: {} as ExtendProps<SelectProps>,
setup(_, { slots, emit }) {
const key = ref("ssr");
onMounted(() => {
key.value = "csr";
});
return () => (
<SelectPrimitive key={key.value}>
{({
selectedOption,
}: {
selectedOption?: { value: string; label: string };
}) => (
<SelectContext selectedOption={selectedOption}>
<div class="grid gap-2">{slots.default?.()}</div>
</SelectContext>
)}
</SelectPrimitive>
);
},
});
const SelectTrigger = defineComponent({
setup(_, { slots, attrs }) {
return () => (
<SelectTriggerPrimitive>
<button
class={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
attrs.class ?? ""
)}
>
{slots.default?.()}
<Chevron />
</button>
</SelectTriggerPrimitive>
);
},
});
const SelectValue = defineComponent({
props: {
placeholder: {
type: String,
default: "Select an option",
},
},
setup({ placeholder }) {
const selectedOption = inject(selectedOptionKey);
return () => (
<div>
{selectedOption?.value ? selectedOption.value.label : placeholder}
</div>
);
},
});
const SelectContent = defineComponent({
setup(_, { slots, attrs }) {
return () => (
<Teleport to="body">
<SelectPositioner>
<SelectContentPrimitive
class={cn(
"z-50 min-w-[8rem] focus:outline-none overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md w-[var(--reference-width)]",
attrs.class ?? ""
)}
>
{slots.default?.()}
</SelectContentPrimitive>
</SelectPositioner>
</Teleport>
);
},
});
const SelectOption = defineComponent({
props: {
value: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
},
setup(props, { attrs }) {
const selectedOption = inject(selectedOptionKey);
const isSelected = computed(
() => selectedOption?.value?.value === props.value
);
return () => (
<SelectOptionPrimitive
value={props.value}
class={cn(
"relative flex justify-between w-full cursor-default select-none items-center rounded-sm py-1.5 pr-8 pl-2 text-sm outline-none data-[focus]:bg-accent data-[focus]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
attrs.class ?? ""
)}
label={props.label}
>
{props.label}
{isSelected.value && (
<span class="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<Check />
</span>
)}
</SelectOptionPrimitive>
);
},
});
const SelectLabel = defineComponent({
setup(_, { slots, attrs }) {
return () => {
return (
<SelectLabelPrimitive class={cn(labelClasses, attrs.class ?? "")}>
{slots.default?.()}
</SelectLabelPrimitive>
);
};
},
});
export {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectOption,
SelectLabel,
};
Usage
import {
Select,
SelectTrigger,
SelectContent,
SelectOption,
SelectValue,
} from "~/components/ui/select";
<Select>
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectOption value="apple" label="Apple" />
<SelectOption value="banana" label="Banana" />
<SelectOption value="blueberry" label="Blueberry" />
</SelectContent>
</Select>