Appearance
@carrot/design-vue-dropdowns
Vue 3 dropdown components for the Carrot Design System. Compound component architecture with provide/inject, full keyboard navigation, and reusable composables.
Installation
ts
import {
CarrotDropdown,
CarrotDropdownTrigger,
CarrotDropdownMenu,
CarrotDropdownItem,
CarrotDropdownGroup,
CarrotDropdownDivider,
} from "@carrot/design-vue-dropdowns";Requires the CSS layer from @carrot/design-components-dropdowns.
Components
CarrotDropdown
Root compound component. Manages open state, provides context to children via injection.
Usage
vue
<!-- Basic -->
<CarrotDropdown>
<CarrotDropdownTrigger>
<CarrotButton>Options</CarrotButton>
</CarrotDropdownTrigger>
<CarrotDropdownMenu>
<CarrotDropdownItem @select="handleEdit">Edit</CarrotDropdownItem>
<CarrotDropdownItem @select="handleDuplicate">Duplicate</CarrotDropdownItem>
<CarrotDropdownDivider />
<CarrotDropdownItem variant="danger" @select="handleDelete">Delete</CarrotDropdownItem>
</CarrotDropdownMenu>
</CarrotDropdown>
<!-- Controlled open state -->
<CarrotDropdown v-model:open="isOpen">
...
</CarrotDropdown>
<!-- With scoped slot for external state access -->
<CarrotDropdown v-slot="{ isOpen, toggle, close }">
<CarrotDropdownTrigger>
<CarrotButton>{{ isOpen ? 'Close' : 'Open' }}</CarrotButton>
</CarrotDropdownTrigger>
<CarrotDropdownMenu>
<CarrotDropdownItem>Option</CarrotDropdownItem>
</CarrotDropdownMenu>
</CarrotDropdown>
<!-- Keep open after selection -->
<CarrotDropdown :close-on-select="false">
...
</CarrotDropdown>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 |
CarrotDropdownTrigger
Wraps the trigger element. Handles click-to-toggle, aria-haspopup, aria-expanded, and keyboard open (ArrowDown/Enter/Space).
vue
<CarrotDropdownTrigger>
<CarrotButton>Click me</CarrotButton>
</CarrotDropdownTrigger>CarrotDropdownMenu
The popup panel. Auto-focuses the first item on open. Full keyboard navigation via useMenuKeyboard.
vue
<CarrotDropdownMenu align="bottom-end">
<CarrotDropdownItem>Option A</CarrotDropdownItem>
<CarrotDropdownItem>Option B</CarrotDropdownItem>
</CarrotDropdownMenu>Props
| Prop | Type | Default | Description |
|---|---|---|---|
align | DropdownAlignment | "bottom-start" | Menu positioning |
full | boolean | false | Match trigger width |
CarrotDropdownItem
Interactive menu item — polymorphic (<button> or <a> when href provided).
vue
<!-- Standard item -->
<CarrotDropdownItem value="edit" @select="handleSelect">
<template #icon><EditIcon /></template>
Edit
<template #trail><kbd>⌘E</kbd></template>
</CarrotDropdownItem>
<!-- Link item -->
<CarrotDropdownItem href="/settings">Settings</CarrotDropdownItem>
<!-- With description -->
<CarrotDropdownItem>
Export
<template #description>Download as CSV</template>
</CarrotDropdownItem>
<!-- Danger variant -->
<CarrotDropdownItem variant="danger">Delete</CarrotDropdownItem>
<!-- Disabled -->
<CarrotDropdownItem disabled>Unavailable</CarrotDropdownItem>
<!-- Selected / checked -->
<CarrotDropdownItem :selected="isChecked">Toggle option</CarrotDropdownItem>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) |
CarrotDropdownGroup
Groups items with an optional heading.
vue
<CarrotDropdownGroup label="Actions">
<CarrotDropdownItem>Edit</CarrotDropdownItem>
<CarrotDropdownItem>Duplicate</CarrotDropdownItem>
</CarrotDropdownGroup>CarrotDropdownDivider
Visual separator between items.
vue
<CarrotDropdownDivider />Composables
useClickOutside(targets, handler, enabled?)
Detects clicks outside multiple element refs using composedPath() for shadow DOM compatibility.
ts
import { useClickOutside } from "@carrot/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 "@carrot/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 |
|---|---|
@carrot/design-components-dropdowns | CSS tokens + generated styles |
Build
bash
npm run buildBuilt with Vite + vite-plugin-dts. Outputs ES module with TypeScript declarations.
Usage Guide
Setup
ts
import {
CarrotDropdown,
CarrotDropdownTrigger,
CarrotDropdownMenu,
CarrotDropdownItem,
CarrotDropdownGroup,
CarrotDropdownDivider,
} from "@carrot/design-vue-dropdowns";
import "@carrot/design-components-dropdowns";CarrotDropdown
Basic action menu
vue
<script setup lang="ts">
import {
CarrotDropdown,
CarrotDropdownTrigger,
CarrotDropdownMenu,
CarrotDropdownItem,
CarrotDropdownDivider,
} from "@carrot/design-vue-dropdowns";
import { CarrotButton } from "@carrot/design-vue-buttons";
const emit = defineEmits<{ edit: []; duplicate: []; delete: [] }>();
</script>
<template>
<CarrotDropdown>
<CarrotDropdownTrigger>
<CarrotButton variant="ghost" icon aria-label="Actions">
<MoreIcon />
</CarrotButton>
</CarrotDropdownTrigger>
<CarrotDropdownMenu align="bottom-end">
<CarrotDropdownItem @select="emit('edit')">
<template #icon><EditIcon /></template>
Edit
</CarrotDropdownItem>
<CarrotDropdownItem @select="emit('duplicate')">
<template #icon><CopyIcon /></template>
Duplicate
</CarrotDropdownItem>
<CarrotDropdownDivider />
<CarrotDropdownItem variant="danger" @select="emit('delete')">
<template #icon><TrashIcon /></template>
Delete
</CarrotDropdownItem>
</CarrotDropdownMenu>
</CarrotDropdown>
</template>Controlled dropdown with grouped items
vue
<script setup lang="ts">
import {
CarrotDropdown,
CarrotDropdownTrigger,
CarrotDropdownMenu,
CarrotDropdownItem,
CarrotDropdownGroup,
} from "@carrot/design-vue-dropdowns";
import { CarrotButton } from "@carrot/design-vue-buttons";
import { ref } from "vue";
const isOpen = ref(false);
function onSelect(value: string) {
console.log("selected:", value);
isOpen.value = false;
}
</script>
<template>
<CarrotDropdown v-model:open="isOpen">
<CarrotDropdownTrigger>
<CarrotButton>{{ isOpen ? "Close" : "Sort by" }}</CarrotButton>
</CarrotDropdownTrigger>
<CarrotDropdownMenu>
<CarrotDropdownGroup label="Sort Order">
<CarrotDropdownItem value="asc" @select="onSelect">A → Z</CarrotDropdownItem>
<CarrotDropdownItem value="desc" @select="onSelect">Z → A</CarrotDropdownItem>
</CarrotDropdownGroup>
<CarrotDropdownGroup label="Sort By">
<CarrotDropdownItem value="name" @select="onSelect">Name</CarrotDropdownItem>
<CarrotDropdownItem value="date" @select="onSelect">Date added</CarrotDropdownItem>
<CarrotDropdownItem value="plays" @select="onSelect">Play count</CarrotDropdownItem>
</CarrotDropdownGroup>
</CarrotDropdownMenu>
</CarrotDropdown>
</template>Dropdown with keyboard shortcuts in trail
vue
<template>
<CarrotDropdown>
<CarrotDropdownTrigger>
<CarrotButton variant="secondary">File</CarrotButton>
</CarrotDropdownTrigger>
<CarrotDropdownMenu>
<CarrotDropdownItem value="new">
New
<template #trail><kbd>⌘N</kbd></template>
</CarrotDropdownItem>
<CarrotDropdownItem value="open">
Open
<template #trail><kbd>⌘O</kbd></template>
</CarrotDropdownItem>
<CarrotDropdownItem value="save">
Save
<template #trail><kbd>⌘S</kbd></template>
</CarrotDropdownItem>
</CarrotDropdownMenu>
</CarrotDropdown>
</template>Common Patterns
Dropdown with checkbox-style selection
vue
<script setup lang="ts">
import {
CarrotDropdown,
CarrotDropdownTrigger,
CarrotDropdownMenu,
CarrotDropdownItem,
} from "@carrot/design-vue-dropdowns";
import { CarrotButton } from "@carrot/design-vue-buttons";
import { ref } from "vue";
const selected = ref<string[]>([]);
function toggle(value: string) {
if (selected.value.includes(value)) {
selected.value = selected.value.filter((v) => v !== value);
} else {
selected.value = [...selected.value, value];
}
}
</script>
<template>
<!-- closeOnSelect=false keeps the menu open while toggling -->
<CarrotDropdown :close-on-select="false">
<CarrotDropdownTrigger>
<CarrotButton>Genres ({{ selected.length }})</CarrotButton>
</CarrotDropdownTrigger>
<CarrotDropdownMenu>
<CarrotDropdownItem
v-for="genre in ['Electronic', 'Jazz', 'Rock', 'Classical']"
:key="genre"
:value="genre"
:selected="selected.includes(genre)"
@select="toggle(genre)"
>
{{ genre }}
</CarrotDropdownItem>
</CarrotDropdownMenu>
</CarrotDropdown>
</template>