Component specification
A flexible dropdown menu component that provides contextual actions and options. It combines the functionality of `ActionDropdown` and `ActionToggle`, offering a unified interface for dropdown menus across n8n.
Overview
Component specification
A flexible dropdown menu component that provides contextual actions and options. It combines the functionality of ActionDropdown and ActionToggle, offering a unified interface for dropdown menus across n8n.
It's built on Reka UI's DropdownMenu for accessibility and interaction patterns.
- Component Name: N8nDropdownMenu
- Figma Component: Figma
- Reka UI Component: DropdownMenu
Public API Definition
Props
id?: stringUnique identifier for the dropdownitems: Array<DropdownMenuItem>Array of menu items to displaymodelValue?: booleanThe controlled open state of the dropdown. Can be bind asv-modeldefaultOpen?: booleanThe open state of the dropdown when initially renderedplacement?: 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end'Dropdown placement relative to trigger |default: 'bottom'trigger?: 'click' | 'hover'How the dropdown is triggered |default: 'click'activatorIcon?: IconOrEmojiIcon or emoji for the default trigger button |default: { type: 'icon', value: 'ellipsis' }disabled?: booleanWhentrue, prevents the user from interacting with dropdownteleported?: booleanWhether to teleport the dropdown to body |default: truemaxHeight?: string | numberMaximum height of the dropdown menuloading?: booleanWhether to show loading stateloadingItemCount?: numberNumber of skeleton items to show when loading |default: 3extraPopperClass?: stringAdditional CSS class for the dropdown popper
Search-specific Props
searchable?: booleanEnable search functionalitysearchPlaceholder?: stringSearch input placeholdersearchDebounce?: numberDebounce delay in ms |default: 300
Events
update:modelValue(open: boolean)Emitted when dropdown open state changesselect(value: T)Emitted when a menu item is selectedsearch(searchTerm: string, itemId?: T)Emitted when search input changes (debounced).itemIdis undefined for root-level search, or the item's ID for sub-menu searchsubmenu:toggle(itemId: T, open: boolean)Emitted when a sub-menu opens or closes. Useful for lazy loading sub-menu content
Exposed Methods
open()Programmatically opens the dropdownclose()Programmatically closes the dropdown
Slots
triggerCustom trigger element (replaces default button)contentComplete custom dropdown content (replaces item list)itemCustom item rendering (replaces default N8nDropdownMenuItem){ item: DropdownMenuItemProps }item-leadingPass-through to N8nDropdownMenuItem{ item: DropdownMenuItemProps, ui: { class: string } }item-trailingPass-through to N8nDropdownMenuItem{ item: DropdownMenuItemProps, ui: { class: string } }loadingCustom loading stateemptyCustom empty state when no items
Types
type IconOrEmoji =
| { type: 'icon'; value: IconName }
| { type: 'emoji'; value: string };
type DropdownMenuItemProps = {
id: T;
label: string;
icon?: IconOrEmoji;
disabled?: boolean;
divided?: boolean; // Shows separator above item
checked?: boolean; // Shows checkmark
class?: string | Record<string, boolean> | Array<string>;
// Sub-menu support
children?: Array<DropdownMenuItemProps>;
loading?: boolean;
loadingItemCount?: number;
searchable?: boolean;
searchPlaceholder?: string;
}
N8nDropdownMenuItem
A companion component for rendering individual dropdown items with full slot-based customization.
Props
id: TUnique identifier for the itemlabel?: stringDisplay text for the itemicon?: IconOrEmojiIcon or emoji displayed before the labeldisabled?: booleanWhether the item is disableddivided?: booleanWhether to show a separator above the item (fromActionDropdownItem)checked?: booleanWhether to show a checkmark indicator (fromActionDropdownItem)class?: stringAdditional CSS classeschildren?: Array<DropdownMenuItemProps>Nested menu items (creates a sub-menu)loading?: booleanWhether to show loading state in sub-menuloadingItemCount?: numberNumber of skeleton items when loading |default: 3searchable?: booleanEnable search functionality for this item's childrensearchPlaceholder?: stringSearch input placeholder
Events
select(value: T)Emitted when item or child is selectedsearch(searchTerm: string, itemId: T)Emitted when sub-menu search changesupdate:subMenuOpen(open: boolean)Emitted when sub-menu open state changes
Slots
item-leadingContent before the label (default: icon if provided){ item: DropdownMenuItemProps, ui: { class: string } }item-trailingContent after the label (badges, shortcuts, etc.){ item: DropdownMenuItemProps, ui: { class: string } }
Examples
Basic example with items
<script setup lang="ts">
const dropdownItems = ref([
{ id: 'edit', label: 'Edit', icon: { type: 'icon', value: 'pen' } },
{ id: 'duplicate', label: 'Duplicate', icon: { type: 'icon', value: 'copy' } },
{ id: 'delete', label: 'Delete', icon: { type: 'icon', value: 'trash' }, divided: true }
])
const handleSelect = (action: string) => {
console.log('Selected:', action)
}
</script>
<template>
</template>
With custom trigger
<script setup lang="ts">
const isOpen = ref(false)
const items = ref([
{ id: 'profile', label: 'Profile', icon: { type: 'icon', value: 'user' } },
{ id: 'settings', label: 'Settings', icon: { type: 'icon', value: 'cog' } },
{ id: 'logout', label: 'Sign out', icon: { type: 'icon', value: 'sign-out' }, divided: true }
])
</script>
<template>
<template #trigger>
Account
</template>
</template>
With emoji activator
<script setup lang="ts">
const items = ref([
{ id: 'option-1', label: 'Option 1' },
{ id: 'option-2', label: 'Option 2' }
])
</script>
<template>
</template>
With badges and shortcuts using slots
<script setup lang="ts">
const items = ref([
{ id: 'save', label: 'Save', icon: { type: 'icon', value: 'save' } },
{ id: 'share', label: 'Share', icon: { type: 'icon', value: 'share' } },
{ id: 'pro', label: 'Pro Feature', icon: { type: 'icon', value: 'star' }, disabled: true }
])
const handleUpgrade = () => {
// Show upgrade modal
}
</script>
<template>
<template #item-trailing="{ item, ui }">
New
PRO
</template>
</template>
Sub-menu example
<script setup lang="ts">
const items = ref([
{ id: 'file', label: 'File', icon: { type: 'icon', value: 'file' } },
{
id: 'export',
label: 'Export as...',
icon: { type: 'icon', value: 'download' },
children: [
{ id: 'pdf', label: 'PDF' },
{ id: 'csv', label: 'CSV' },
{ id: 'json', label: 'JSON' }
]
},
])
</script>
<template>
</template>
Searchable sub-menu (async search)
<script setup lang="ts">
const users = ref([])
const loading = ref(false)
const items = computed(() => [
{
id: 'select-user',
label: 'Assign to...',
searchable: true,
searchPlaceholder: 'Search users...',
loading: loading.value,
children: users.value
},
])
const handleSearch = async (term: string, itemId?: string) => {
if (itemId === 'select-user') {
loading.value = true
users.value = await fetchUsers(term)
loading.value = false
}
}
</script>
<template>
</template>
Loading state
<script setup lang="ts">
const loading = ref(true)
const items = ref([])
onMounted(async () => {
// Fetch items
items.value = await fetchMenuItems()
loading.value = false
})
</script>
<template>
</template>
Lazy loading sub-menu items
<script setup lang="ts">
const recentFilesLoading = ref(true)
const recentFiles = ref([])
const items = computed(() => [
{ id: 'new', label: 'New File', icon: { type: 'icon', value: 'plus' } },
{
id: 'recent',
label: 'Recent Files',
icon: { type: 'icon', value: 'clock' },
loading: recentFilesLoading.value,
loadingItemCount: 4,
children: recentFiles.value
},
{ id: 'settings', label: 'Settings', icon: { type: 'icon', value: 'cog' } }
])
const handleSubmenuToggle = async (itemId: string, open: boolean) => {
// Only fetch when sub-menu opens and data hasn't been loaded
if (itemId === 'recent' && open && recentFilesLoading.value) {
recentFiles.value = await fetchRecentFiles()
recentFilesLoading.value = false
}
}
// Reset on dropdown close to show loading again next time
const handleOpenChange = (open: boolean) => {
if (!open) {
recentFilesLoading.value = true
recentFiles.value = []
}
}
</script>
<template>
</template>
Using N8nDropdownMenuItem for full control
<script setup lang="ts">
const items = ref([
{ id: 'option-1', label: 'Option 1' },
{ id: 'option-2', label: 'Option 2' }
])
</script>
<template>
<template #item="{ item }">
<template #item-leading="{ ui }">
</template>
<template #item-trailing="{ ui }">
Custom
</template>
</template>
</template>
Migration Guide
From ActionDropdown
<template #item-trailing="{ item, ui }">
{{ item.badge }}
</template>
From ActionToggle
Implementation Notes
closeOnParentScrollis not yet implemented - requiresuseParentScrollcomposable migration- The new
update:modelValuereplaces bothvisible-changeandvisibleChangeevents - Search is opt-in via
searchableprop. Both root-level and sub-menu search are supported - Search filtering is not built-in - use the
searchevent to filter items externally (e.g., for async search) - The
iconprop now acceptsIconOrEmojitype:{ type: 'icon', value: 'pen' }or{ type: 'emoji', value: '🎉' } - Keyboard navigation in searchable menus uses virtual highlighting (focus stays in search input)
- Non-searchable menus use Reka UI's built-in roving focus for keyboard navigation