Skip to content

@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

PropTypeDefaultDescription
openbooleanControlled open state (v-model:open)
closeOnSelectbooleantrueClose when an item is selected
closeOnClickOutsidebooleantrueClose on outside click
closeOnEscapebooleantrueClose on Escape key

Events

EventPayloadDescription
update:openbooleanOpen state changed
selectunknownItem selected

Scoped Slot

PropertyTypeDescription
isOpenbooleanCurrent open state
toggle() => voidToggle open/close
close() => voidClose 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

PropTypeDefaultDescription
alignDropdownAlignment"bottom-start"Menu positioning
fullbooleanfalseMatch 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

PropTypeDefaultDescription
valueunknownValue emitted on select
variantDropdownItemVariantVisual variant (e.g. "danger")
hrefstringRender as link
disabledbooleanfalseDisabled state
selectedbooleanfalseSelected/checked state

Events

EventPayloadDescription
selectunknownItem clicked (value prop)

Slots

SlotDescription
defaultItem label
iconLeading icon
descriptionSecondary description text
trailTrailing 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

KeyAction
ArrowDown / Enter / Space (on trigger)Open menu
ArrowDown / ArrowUpMove focus between items
Home / EndJump to first / last item
Enter / Space (on item)Select item
EscapeClose menu, return focus to trigger
TabClose menu
Any letterTypeahead — focus next matching item

Dependencies

PackagePurpose
@vibe-labs/design-components-dropdownsCSS tokens + generated styles

Build

bash
npm run build

Built with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.