Select Dropdown
The SelectDropdown is a visually customized version of the Select component, eliminating the reliance on the native "select" HTML tag. Despite the stylistic changes, the functionality of the component remains unchanged, allowing users to make a single selection from a dropdown list. The input can be marked as required and may include hints or additional information.
Please note that the example block provided can also serve as a base component, offering flexibility for implementation within the project based on specific requirements and design considerations.
If you need to make this field required, it is crucial to communicate this intention clearly to your end users. You can find more information about required form fields in our guide here.
Accessibility notes
The SelectDropdown fully supports the use of the keyboard.
Basic usage
Select Dropdown with preselected option.
<template>
<label class="font-medium typography-label-sm" :for="id">Product</label>
<div ref="referenceRef" class="relative">
<div
:id="id"
ref="selectTriggerRef"
role="combobox"
:aria-controls="listboxId"
:aria-expanded="isOpen"
aria-label="Select one option"
:aria-activedescendant="selectedOption ? `${listboxId}-${selectedOption.value}` : undefined"
class="mt-0.5 flex items-center gap-8 relative font-normal typography-text-base ring-1 ring-neutral-300 ring-inset rounded-md py-2 px-4 hover:ring-primary-700 active:ring-primary-700 active:ring-2 focus:ring-primary-700 focus:ring-2 focus-visible:outline focus-visible:outline-offset cursor-pointer"
tabindex="0"
@keydown.space="toggle()"
@click="toggle()"
>
<template v-if="selectedOption">{{ selectedOption.label }}</template>
<span v-else class="text-neutral-500">Choose from the list</span>
<SfIconExpandMore
class="ml-auto text-neutral-500 transition-transform ease-in-out duration-300"
:class="{ 'rotate-180': isOpen }"
/>
</div>
<ul
v-show="isOpen"
:id="listboxId"
ref="floatingRef"
role="listbox"
aria-label="Select one option"
class="w-full py-2 rounded-md shadow-md border border-neutral-100 bg-white z-10"
:style="dropdownStyle"
>
<SfListItem
v-for="option in options"
:id="`${listboxId}-${option.value}`"
:key="option.value"
role="option"
tabindex="0"
:aria-selected="option.value === selectedOption?.value"
class="block"
:class="{ 'font-medium': option.value === selectedOption?.value }"
@click="selectOption(option)"
@keydown.enter="selectOption(option)"
@keydown.space="selectOption(option)"
>
{{ option.label }}
<template #suffix>
<SfIconCheck v-if="option.value === selectedOption?.value" class="text-primary-700" />
</template>
</SfListItem>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref, type Ref } from 'vue';
import { unrefElement } from '@vueuse/core';
import {
useDropdown,
useDisclosure,
SfIconExpandMore,
SfListItem,
SfIconCheck,
useId,
useTrapFocus,
} from '@storefront-ui/vue';
type SelectOption = {
label: string;
value: string;
};
const options: SelectOption[] = [
{
label: 'Startup',
value: 'startup',
},
{
label: 'Business',
value: 'business',
},
{
label: 'Enterprise',
value: 'enterprise',
},
];
const { close, toggle, isOpen } = useDisclosure({ initialValue: false });
const selectedOption = ref<SelectOption>(options[0]);
const id = useId();
const listboxId = `select-dropdown-${id}`;
const selectTriggerRef = ref<HTMLDivElement>();
const {
referenceRef,
floatingRef,
style: dropdownStyle,
} = useDropdown({
isOpen,
onClose: close,
});
useTrapFocus(floatingRef as Ref<HTMLUListElement>, {
arrowKeysUpDown: true,
activeState: isOpen,
initialFocusContainerFallback: true,
});
const selectOption = (option: SelectOption) => {
selectedOption.value = option;
close();
unrefElement(selectTriggerRef as Ref<HTMLDivElement>)?.focus();
};
</script>
With placeholder
Adding placeholder might be helpful and informative for end users.
<template>
<label class="font-medium typography-label-sm" :for="id">Delivery</label>
<div ref="referenceRef" class="relative">
<div
:id="id"
ref="selectTriggerRef"
role="combobox"
:aria-controls="listboxId"
:aria-expanded="isOpen"
aria-label="Select one option"
:aria-activedescendant="selectedOption ? `${listboxId}-${selectedOption.value}` : undefined"
class="mt-0.5 flex items-center gap-8 relative font-normal typography-text-base ring-1 ring-neutral-300 ring-inset rounded-md py-2 px-4 hover:ring-primary-700 active:ring-primary-700 active:ring-2 focus:ring-primary-700 focus:ring-2 focus-visible:outline focus-visible:outline-offset cursor-pointer"
tabindex="0"
@keydown.space="toggle()"
@click="toggle()"
>
<template v-if="selectedOption">{{ selectedOption.label }}</template>
<span v-else class="text-neutral-500">Choose from the list</span>
<SfIconExpandMore
class="ml-auto text-neutral-500 transition-transform ease-in-out duration-300"
:class="{ 'rotate-180': isOpen }"
/>
</div>
<ul
v-show="isOpen"
:id="listboxId"
ref="floatingRef"
role="listbox"
aria-label="Select one option"
class="w-full py-2 rounded-md shadow-md border border-neutral-100 bg-white z-10"
:style="dropdownStyle"
>
<SfListItem
v-for="option in options"
:id="`${listboxId}-${option.value}`"
:key="option.value"
role="option"
tabindex="0"
:aria-selected="option.value === selectedOption?.value"
class="block"
:class="{ 'font-medium': option.value === selectedOption?.value }"
@click="selectOption(option)"
@keydown.enter="selectOption(option)"
@keydown.space="selectOption(option)"
>
{{ option.label }}
<template #suffix>
<SfIconCheck v-if="option.value === selectedOption?.value" class="text-primary-700" />
</template>
</SfListItem>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref, type Ref } from 'vue';
import { unrefElement } from '@vueuse/core';
import {
useDropdown,
useDisclosure,
SfIconExpandMore,
SfListItem,
SfIconCheck,
useId,
useTrapFocus,
} from '@storefront-ui/vue';
type SelectOption = {
label: string;
value: string;
};
const options: SelectOption[] = [
{
label: 'Today',
value: 'today',
},
{
label: 'Tomorrow',
value: 'tomorrow',
},
{
label: 'Anytime',
value: 'anytime',
},
];
const { close, toggle, isOpen } = useDisclosure({ initialValue: false });
const selectedOption = ref<SelectOption>();
const id = useId();
const listboxId = `select-dropdown-${id}`;
const selectTriggerRef = ref<HTMLDivElement>();
const {
referenceRef,
floatingRef,
style: dropdownStyle,
} = useDropdown({
isOpen,
onClose: close,
});
useTrapFocus(floatingRef as Ref<HTMLUListElement>, {
arrowKeysUpDown: true,
activeState: isOpen,
initialFocusContainerFallback: true,
});
const selectOption = (option: SelectOption) => {
selectedOption.value = option;
close();
unrefElement(selectTriggerRef as Ref<HTMLDivElement>)?.focus();
};
</script>
Invalid state
Provide visual cues for end users to indicate occuring error.
<template>
<label class="font-medium typography-label-sm" :for="id">Delivery</label>
<div ref="referenceRef" class="relative">
<div
:id="id"
ref="selectTriggerRef"
role="combobox"
:aria-controls="listboxId"
:aria-expanded="isOpen"
aria-label="Select one option"
:aria-activedescendant="selectedOption ? `${listboxId}-${selectedOption.value}` : undefined"
class="mt-0.5 flex items-center gap-8 relative ring-inset rounded-md py-2 px-4 font-normal typography-text-base focus-visible:outline focus-visible:outline-offset cursor-pointer"
:class="
isValid
? 'ring-1 ring-neutral-300 hover:ring-primary-700 active:ring-primary-700 active:ring-2 focus:ring-primary-700 focus:ring-2'
: 'ring-2 ring-negative-700'
"
tabindex="0"
@keydown.space="toggle()"
@click="toggle()"
>
<template v-if="selectedOption">{{ selectedOption.label }}</template>
<span v-else class="text-neutral-500">Choose from the list</span>
<SfIconExpandMore
class="ml-auto text-neutral-500 transition-transform ease-in-out duration-300"
:class="{ 'rotate-180': isOpen }"
/>
</div>
<ul
v-show="isOpen"
:id="listboxId"
ref="floatingRef"
role="listbox"
aria-label="Select one option"
class="w-full py-2 rounded-md shadow-md border border-neutral-100 bg-white z-10"
:style="dropdownStyle"
>
<SfListItem
v-for="option in options"
:id="`${listboxId}-${option.value}`"
:key="option.value"
role="option"
tabindex="0"
:aria-selected="option.value === selectedOption?.value"
class="block"
:class="{ 'font-medium': option.value === selectedOption?.value }"
@click="selectOption(option)"
@keydown.enter="selectOption(option)"
@keydown.space="selectOption(option)"
>
{{ option.label }}
<template #suffix>
<SfIconCheck v-if="option.value === selectedOption?.value" class="text-primary-700" />
</template>
</SfListItem>
</ul>
</div>
<p v-if="!isValid" class="text-negative-700 typography-text-sm font-medium mt-0.5">No option selected</p>
</template>
<script lang="ts" setup>
import { ref, computed, type Ref } from 'vue';
import { unrefElement } from '@vueuse/core';
import {
useDropdown,
useDisclosure,
SfIconExpandMore,
SfListItem,
SfIconCheck,
useId,
useTrapFocus,
} from '@storefront-ui/vue';
type SelectOption = {
label: string;
value: string;
};
const options: SelectOption[] = [
{
label: 'Today',
value: 'today',
},
{
label: 'Tomorrow',
value: 'tomorrow',
},
{
label: 'Anytime',
value: 'anytime',
},
];
const { close, toggle, isOpen } = useDisclosure({ initialValue: false });
const selectedOption = ref<SelectOption>();
const id = useId();
const listboxId = `select-dropdown-${id}`;
const isValid = computed(() => !!selectedOption.value);
const selectTriggerRef = ref<HTMLDivElement>();
const {
referenceRef,
floatingRef,
style: dropdownStyle,
} = useDropdown({
isOpen,
onClose: close,
});
useTrapFocus(floatingRef as Ref<HTMLUListElement>, {
arrowKeysUpDown: true,
activeState: isOpen,
initialFocusContainerFallback: true,
});
const selectOption = (option: SelectOption) => {
selectedOption.value = option;
close();
unrefElement(selectTriggerRef as Ref<HTMLDivElement>)?.focus();
};
</script>
Disabled state
Differentiate disabled state to smooth UX experience. In such case, keyboard navigation becomes disabled as well and an aria-disabled="true"
is specified for better accessibility.
<template>
<label class="font-medium typography-label-sm" :class="{ 'text-disabled-900': isDisabled }" :for="id">Delivery</label>
<div ref="referenceRef" class="relative">
<div
:id="id"
ref="selectTriggerRef"
role="combobox"
:aria-controls="listboxId"
:aria-expanded="isOpen"
:aria-disabled="isDisabled"
aria-label="Select one option"
:aria-activedescendant="selectedOption ? `${listboxId}-${selectedOption.value}` : undefined"
class="mt-0.5 flex items-center gap-8 relative font-normal typography-text-base ring-1 ring-inset rounded-md py-2 px-4"
:class="
isDisabled
? 'bg-disabled-100 ring-disabled-300 cursor-not-allowed'
: 'ring-neutral-300 hover:ring-primary-700 active:ring-primary-700 active:ring-2 focus:ring-primary-700 focus:ring-2 cursor-pointer'
"
tabindex="0"
@keydown.space="!isDisabled && toggle()"
@click="!isDisabled && toggle()"
>
<template v-if="selectedOption">{{ selectedOption.label }}</template>
<span v-else :class="isDisabled ? 'text-disabled-500' : 'text-neutral-500'">Choose from the list</span>
<SfIconExpandMore
class="ml-auto transition-transform ease-in-out duration-300"
:class="[{ 'rotate-180': isOpen }, isDisabled ? 'text-disabled-500' : 'text-neutral-500']"
/>
</div>
<ul
v-show="isOpen"
:id="listboxId"
ref="floatingRef"
role="listbox"
aria-label="Select one option"
class="w-full py-2 rounded-md shadow-md border border-neutral-100 bg-white z-10"
:style="dropdownStyle"
>
<SfListItem
v-for="option in options"
:id="`${listboxId}-${option.value}`"
:key="option.value"
role="option"
tabindex="0"
:aria-selected="option.value === selectedOption?.value"
class="block"
:class="{ 'font-medium': option.value === selectedOption?.value }"
@click="selectOption(option)"
@keydown.enter="selectOption(option)"
@keydown.space="selectOption(option)"
>
{{ option.label }}
<template #suffix>
<SfIconCheck v-if="option.value === selectedOption?.value" class="text-primary-700" />
</template>
</SfListItem>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref, type Ref } from 'vue';
import { unrefElement } from '@vueuse/core';
import {
useDropdown,
useDisclosure,
SfIconExpandMore,
SfListItem,
SfIconCheck,
useId,
useTrapFocus,
} from '@storefront-ui/vue';
type SelectOption = {
label: string;
value: string;
};
const options: SelectOption[] = [
{
label: 'Today',
value: 'today',
},
{
label: 'Tomorrow',
value: 'tomorrow',
},
{
label: 'Anytime',
value: 'anytime',
},
];
const { close, toggle, isOpen } = useDisclosure({ initialValue: false });
const isDisabled = ref(true);
const selectedOption = ref<SelectOption>();
const id = useId();
const listboxId = `select-dropdown-${id}`;
const selectTriggerRef = ref<HTMLDivElement>();
const {
referenceRef,
floatingRef,
style: dropdownStyle,
} = useDropdown({
isOpen,
onClose: close,
});
useTrapFocus(floatingRef as Ref<HTMLUListElement>, {
arrowKeysUpDown: true,
activeState: isOpen,
initialFocusContainerFallback: true,
});
const selectOption = (option: SelectOption) => {
selectedOption.value = option;
close();
unrefElement(selectTriggerRef as Ref<HTMLDivElement>)?.focus();
};
</script>