Appearance
@vibe-labs/design-vue-dropdowns
Vue 3 dropdown components for the Vibe Design System. Compound component architecture with provide/inject, full keyboard navigation, and reusable composables.
Installation
ts
import {
VibeDropdown,
VibeDropdownTrigger,
VibeDropdownMenu,
VibeDropdownItem,
VibeDropdownGroup,
VibeDropdownDivider,
} from "@vibe-labs/design-vue-dropdowns";Requires the CSS layer from @vibe-labs/design-components-dropdowns.
Components
VibeDropdown
Root compound component. Manages open state, provides context to children via injection.
Usage
vue
<!-- Basic -->
<VibeDropdown>
<VibeDropdownTrigger>
<VibeButton>Options</VibeButton>
</VibeDropdownTrigger>
<VibeDropdownMenu>
<VibeDropdownItem @select="handleEdit">Edit</VibeDropdownItem>
<VibeDropdownItem @select="handleDuplicate">Duplicate</VibeDropdownItem>
<VibeDropdownDivider />
<VibeDropdownItem variant="danger" @select="handleDelete">Delete</VibeDropdownItem>
</VibeDropdownMenu>
</VibeDropdown>
<!-- Controlled open state -->
<VibeDropdown v-model:open="isOpen">
...
</VibeDropdown>
<!-- With scoped slot for external state access -->
<VibeDropdown v-slot="{ isOpen, toggle, close }">
<VibeDropdownTrigger>
<VibeButton>{{ isOpen ? 'Close' : 'Open' }}</VibeButton>
</VibeDropdownTrigger>
<VibeDropdownMenu>
<VibeDropdownItem>Option</VibeDropdownItem>
</VibeDropdownMenu>
</VibeDropdown>
<!-- Keep open after selection -->
<VibeDropdown :close-on-select="false">
...
</VibeDropdown>Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state (v-model:open) |
closeOnSelect | boolean | true | Close when an item is selected |
closeOnClickOutside | boolean | true | Close on outside click |
closeOnEscape | boolean | true | Close on Escape key |
Events
| Event | Payload | Description |
|---|---|---|
update:open | boolean | Open state changed |
select | unknown | Item selected |
Scoped Slot
| Property | Type | Description |
|---|---|---|
isOpen | boolean | Current open state |
toggle | () => void | Toggle open/close |
close | () => void | Close the dropdown |
VibeDropdownTrigger
Wraps the trigger element. Handles click-to-toggle, aria-haspopup, aria-expanded, and keyboard open (ArrowDown/Enter/Space).
vue
<VibeDropdownTrigger>
<VibeButton>Click me</VibeButton>
</VibeDropdownTrigger>VibeDropdownMenu
The popup panel. Auto-focuses the first item on open. Full keyboard navigation via useMenuKeyboard.
vue
<VibeDropdownMenu align="bottom-end">
<VibeDropdownItem>Option A</VibeDropdownItem>
<VibeDropdownItem>Option B</VibeDropdownItem>
</VibeDropdownMenu>Props
| Prop | Type | Default | Description |
|---|---|---|---|
align | DropdownAlignment | "bottom-start" | Menu positioning |
full | boolean | false | Match trigger width |
VibeDropdownItem
Interactive menu item — polymorphic (<button> or <a> when href provided).
vue
<!-- Standard item -->
<VibeDropdownItem value="edit" @select="handleSelect">
<template #icon><EditIcon /></template>
Edit
<template #trail><kbd>⌘E</kbd></template>
</VibeDropdownItem>
<!-- Link item -->
<VibeDropdownItem href="/settings">Settings</VibeDropdownItem>
<!-- With description -->
<VibeDropdownItem>
Export
<template #description>Download as CSV</template>
</VibeDropdownItem>
<!-- Danger variant -->
<VibeDropdownItem variant="danger">Delete</VibeDropdownItem>
<!-- Disabled -->
<VibeDropdownItem disabled>Unavailable</VibeDropdownItem>
<!-- Selected / checked -->
<VibeDropdownItem :selected="isChecked">Toggle option</VibeDropdownItem>Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | unknown | — | Value emitted on select |
variant | DropdownItemVariant | — | Visual variant (e.g. "danger") |
href | string | — | Render as link |
disabled | boolean | false | Disabled state |
selected | boolean | false | Selected/checked state |
Events
| Event | Payload | Description |
|---|---|---|
select | unknown | Item clicked (value prop) |
Slots
| Slot | Description |
|---|---|
default | Item label |
icon | Leading icon |
description | Secondary description text |
trail | Trailing content (shortcuts, badges) |
VibeDropdownGroup
Groups items with an optional heading.
vue
<VibeDropdownGroup label="Actions">
<VibeDropdownItem>Edit</VibeDropdownItem>
<VibeDropdownItem>Duplicate</VibeDropdownItem>
</VibeDropdownGroup>VibeDropdownDivider
Visual separator between items.
vue
<VibeDropdownDivider />Composables
useClickOutside(targets, handler, enabled?)
Detects clicks outside multiple element refs using composedPath() for shadow DOM compatibility.
ts
import { useClickOutside } from "@vibe-labs/design-vue-dropdowns";
const el = ref<HTMLElement | null>(null);
useClickOutside(
[el],
() => console.log("clicked outside"),
() => isActive.value,
);useMenuKeyboard(menuEl, opts?)
Full keyboard navigation for menu-like components: ArrowUp/Down, Home/End, Escape, Tab, and single-character typeahead.
ts
import { useMenuKeyboard } from "@vibe-labs/design-vue-dropdowns";
const menuEl = ref<HTMLElement | null>(null);
const { onKeydown, focusFirst } = useMenuKeyboard(menuEl, {
onEscape: () => close(),
onTab: () => close(),
});Keyboard Navigation
| Key | Action |
|---|---|
| ArrowDown / Enter / Space (on trigger) | Open menu |
| ArrowDown / ArrowUp | Move focus between items |
| Home / End | Jump to first / last item |
| Enter / Space (on item) | Select item |
| Escape | Close menu, return focus to trigger |
| Tab | Close menu |
| Any letter | Typeahead — focus next matching item |
Dependencies
| Package | Purpose |
|---|---|
@vibe-labs/design-components-dropdowns | CSS tokens + generated styles |
Build
bash
npm run buildBuilt with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.