Basic Dropdown
A dropdown component built on Nuxt UI's USelect and Reka UI primitives, with a rotating chevron animation and custom item rendering that includes icons and a checkmark indicator. It supports both controlled (v-model) and uncontrolled modes, with motion-v powered animations.
Usage
Provide an array of items with id, label, and optional icon. Use v-model for controlled selection, or listen to @change for uncontrolled mode.
<template>
<div class="flex w-full max-w-md justify-center">
<MBasicDropdown
:items="items"
label="Select option"
@change="onChange"
/>
</div>
</template>
<script setup lang="ts">
const items = [
{ id: '1', label: 'Option 1', icon: 'i-lucide-circle' },
{ id: '2', label: 'Option 2', icon: 'i-lucide-square' },
{ id: '3', label: 'Option 3', icon: 'i-lucide-triangle' },
]
const onChange = (_item) => {
// handle selection
}
</script>
Customize
Customize
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | undefined | Additional CSS classes applied to the trigger. |
items | DropdownItem[] | required | Array of dropdown items to display. |
modelValue | DropdownItem | null | undefined | undefined | Controlled selected item. When undefined, component is uncontrolled. |
label | string | '' | Label shown in the trigger when no item is selected. |
Events
| Event | Payload | Description |
|---|---|---|
change | (item: DropdownItem) => void | Emitted when an item is selected in uncontrolled mode. |
update:modelValue | (value: DropdownItem | null) => void | Emitted when selection changes in controlled mode. |
Slots
| Slot | Props | Description |
|---|---|---|
item | (props: { item: DropdownItem }) => void | Custom item rendering slot. By default, renders icon, label, and checkmark for selected item. |
trailing | (props: { open: boolean }) => void | Custom trailing slot. By default, renders an animated chevron. |
Types
| Prop | Type | Default | Description |
|---|---|---|---|
DropdownItem | { id: string | number, icon?: string, label: string } | - | Interface for dropdown items. |
UI
This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.basicDropdown in app.config.ts.
| Prop | Type | Default | Description |
|---|---|---|---|
ui | object | - | Per-slot classes merged over the theme defaults. |
| Slot | Props | Description |
|---|---|---|
root | - | |
base | - | m-basic-dropdown__trigger min-h-[44px] w-full justify-between gap-2 rounded-lg px-4 text-left |
trailing | - | pe-4 |
content | - | m-basic-dropdown__content rounded-lg |
group | - | m-basic-dropdown__group |
item | - | m-basic-dropdown__item |
itemLabel | - | m-basic-dropdown__item-label |
viewport | - | m-basic-dropdown__viewport |
itemLeadingIcon | - | size-5 shrink-0 |
itemWrapper | - | m-basic-dropdown__item-wrapper |
itemTrailing | - | m-basic-dropdown__item-trailing |
itemTrailingIcon | - | m-basic-dropdown__check |
checkIcon | - | h-4 w-4 text-primary |
chevronIcon | - | h-4 w-4 |
Accessibility
The dropdown uses Nuxt UI's USelect which provides proper ARIA semantics (role="combobox", role="listbox", aria-haspopup, aria-expanded, aria-selected). The custom checkmark SVG includes a <title>Selected</title> for screen readers. Focus is managed by Reka UI primitives with keyboard support (Arrow keys, Enter, Space, Escape). The trigger has aria-label that combines the label and selected item. Focus-visible rings are styled for keyboard navigation.
Performance
- Chevron rotation is animated with motion-v (
duration: 0.2s) and respectsprefers-reduced-motion(falls back to instant rotation when reduced motion is preferred). - Content animations are handled by Reka UI's SelectContent with CSS keyframe animations scoped via data attributes.
- Custom item rendering reuses Reka UI's
SelectItemTextto preserve proper ARIA relationships (labeling the option correctly). - Collision padding (8px) is applied to prevent panel from going off-screen without heavy repositioning.
- The component avoids unnecessary re-renders by using computed properties and proper controlled/uncontrolled state management.
v-bind="$attrs", which are forwarded to the underlying SelectTrigger (since USelect forwards attrs appropriately).