Dropdown Menu
A menu that drops down when triggered, showing a list of options
Basic Menus
Simple dropdown menus with icons and shortcuts
Code Svelte
1
2<script lang="ts">
3 import { DropdownMenu } from "@kareyes/aether";
4 import { User, Settings, LogOut, Plus } from "@kareyes/aether/icons";
5
6 const basicMenuItems = [
7 { label: "Profile", icon: User, onSelect: () => console.log("Profile clicked") },
8 { label: "Settings", icon: Settings, onSelect: () => console.log("Settings clicked") },
9 { type: "separator" },
10 { label: "Logout", icon: LogOut, variant: "destructive", onSelect: () => console.log("Logout clicked") },
11 ];
12</script>
13
14 <div class="flex flex-wrap gap-4">
15 <DropdownMenu
16 triggerText="User Menu"
17 items={basicMenuItems}
18 />
19
20 <DropdownMenu
21 triggerText="File Actions"
22 triggerVariant="default"
23 items={fileMenuItems}
24 />
25
26 <DropdownMenu
27 triggerText="With Icon"
28 triggerIcon={Plus}
29 triggerVariant="secondary"
30 items={basicMenuItems}
31 />
32
33 <DropdownMenu
34 triggerText="No Chevron"
35 showChevron={false}
36 items={basicMenuItems}
37 />
38 </div>Interactive Menus
Menus with checkboxes and radio groups
Status Bar: ✓ Toolbar: ✓ Sidebar: ✗
Current theme: light
Selected plan: free
Code Svelte
1
2<script lang="ts">
3 import { DropdownMenu } from "@kareyes/aether";
4
5 let statusBarChecked = $state(true);
6 let toolbarChecked = $state(true);
7 let sidebarChecked = $state(false);
8 let theme = $state("light");
9
10 const viewMenuItems = $derived([
11 { type: "label", label: "View Options" },
12 { type: "checkbox", label: "Show Status Bar", checked: statusBarChecked, onSelect: () => statusBarChecked = !statusBarChecked },
13 { type: "checkbox", label: "Show Toolbar", checked: toolbarChecked, onSelect: () => toolbarChecked = !toolbarChecked },
14 { type: "checkbox", label: "Show Sidebar", checked: sidebarChecked, onSelect: () => sidebarChecked = !sidebarChecked },
15 ]);
16
17 const themeMenuItems = $derived([
18 {
19 type: "radio",
20 label: "Select Theme",
21 value: theme,
22 items: [
23 { label: "Light", value: "light" },
24 { label: "Dark", value: "dark" },
25 { label: "System", value: "system" },
26 ],
27 onValueChange: (value) => { theme = value; },
28 },
29 ]);
30</script>
31
32 <div class="space-y-4">
33 <div class="flex flex-wrap gap-4">
34 <DropdownMenu
35 triggerText="View Options"
36 items={viewMenuItems}
37 />
38 <div class="flex items-center gap-4 text-sm text-muted-foreground">
39 <span>Status Bar: {statusBarChecked ? '✓' : '✗'}</span>
40 <span>Toolbar: {toolbarChecked ? '✓' : '✗'}</span>
41 <span>Sidebar: {sidebarChecked ? '✓' : '✗'}</span>
42 </div>
43 </div>
44
45 <div class="flex flex-wrap gap-4">
46 <DropdownMenu
47 triggerText={`Theme: ${theme}`}
48 items={themeMenuItems}
49 />
50 <div class="text-sm text-muted-foreground">
51 Current theme: <span class="font-medium">{theme}</span>
52 </div>
53 </div>
54
55 <div class="flex flex-wrap gap-4">
56 <DropdownMenu
57 triggerText={`Plan: ${selectedPlan}`}
58 triggerVariant="outline"
59 items={planMenuItems}
60 />
61 <div class="text-sm text-muted-foreground">
62 Selected plan: <span class="font-medium capitalize">{selectedPlan}</span>
63 </div>
64 </div>
65 </div>Grouped Menu
Organize menu items into labeled groups
Code Svelte
1
2<script lang="ts">
3 import { DropdownMenu } from "@kareyes/aether";
4 import { User, Settings, Bell, Edit, Copy, Trash2, HelpCircle, LogOut } from "@kareyes/aether/icons";
5
6 const groupedMenuItems = [
7 {
8 label: "Account",
9 items: [
10 { label: "Profile", icon: User, onSelect: () => console.log("Profile") },
11 { label: "Settings", icon: Settings, onSelect: () => console.log("Settings") },
12 { label: "Notifications", icon: Bell, onSelect: () => console.log("Notifications") },
13 ],
14 },
15 { type: "separator" },
16 {
17 label: "Actions",
18 items: [
19 { label: "Edit", icon: Edit, onSelect: () => console.log("Edit") },
20 { label: "Copy", icon: Copy, onSelect: () => console.log("Copy") },
21 { label: "Delete", icon: Trash2, variant: "destructive", onSelect: () => console.log("Delete") },
22 ],
23 },
24 { type: "separator" },
25 {
26 items: [
27 { label: "Help", icon: HelpCircle, onSelect: () => console.log("Help") },
28 { label: "Logout", icon: LogOut, variant: "destructive", onSelect: () => console.log("Logout") },
29 ],
30 },
31 ];
32</script>
33
34<DropdownMenu triggerText="Actions" triggerVariant="outline" items={groupedMenuItems} />Complex Combined Menu
Combining groups, radio, checkboxes, and shortcuts
Theme
light
View Options
Status Bar: ✓
Toolbar: ✓
Code Svelte
1
2<script lang="ts">
3 import { DropdownMenu } from "@kareyes/aether";
4 import { FileText, Edit2, Share2, Trash2, Copy, Download, Mail, MessageSquare, Settings } from "@kareyes/aether/icons";
5
6
7 const complexMenuItems = $derived([
8 {
9 label: "My Account",
10 items: [
11 { label: "Profile", icon: User, shortcut: "⌘P", onSelect: () => console.log("Profile") },
12 { label: "Settings", icon: Settings, shortcut: "⌘,", onSelect: () => console.log("Settings") },
13 ],
14 },
15 { type: "separator" as const },
16 {
17 type: "radio" as const,
18 label: "Theme",
19 value: theme,
20 items: [
21 { label: "Light", value: "light" },
22 { label: "Dark", value: "dark" },
23 { label: "System", value: "system" },
24 ],
25 onValueChange: (value: string) => { theme = value; },
26 },
27 { type: "separator" as const },
28 {
29 label: "View",
30 items: [
31 { type: "checkbox" as const, label: "Status Bar", checked: statusBarChecked, onSelect: () => statusBarChecked = !statusBarChecked },
32 { type: "checkbox" as const, label: "Toolbar", checked: toolbarChecked, onSelect: () => toolbarChecked = !toolbarChecked },
33 ],
34 },
35 { type: "separator" as const },
36 {
37 items: [
38 { label: "Help", icon: HelpCircle, shortcut: "⌘?", onSelect: () => console.log("Help") },
39 { label: "Logout", icon: LogOut, variant: "destructive" as const, shortcut: "⌘Q", onSelect: () => console.log("Logout") },
40 ],
41 },
42 ]);
43</script>
44
45 <div class="space-y-4">
46 <DropdownMenu
47 triggerText="Account Settings"
48 triggerVariant="default"
49 items={complexMenuItems}
50 />
51 <div class="grid grid-cols-2 gap-4 max-w-md">
52 <div class="p-3 rounded-md bg-muted/50">
53 <div class="text-xs font-medium text-muted-foreground mb-1">Theme</div>
54 <div class="text-sm font-medium">{theme}</div>
55 </div>
56 <div class="p-3 rounded-md bg-muted/50">
57 <div class="text-xs font-medium text-muted-foreground mb-1">View Options</div>
58 <div class="text-xs space-y-0.5">
59 <div>Status Bar: {statusBarChecked ? '✓' : '✗'}</div>
60 <div>Toolbar: {toolbarChecked ? '✓' : '✗'}</div>
61 </div>
62 </div>
63 </div>
64 </div>Trigger Variants
Different button styles for the menu trigger
Code Svelte
1
2<script lang="ts">
3 import { DropdownMenu } from "@kareyes/aether";
4 import { User, Settings, LogOut } from "@kareyes/aether/icons";
5
6 const items = [
7 { label: "Profile", icon: User, onSelect: () => {} },
8 { label: "Settings", icon: Settings, onSelect: () => {} },
9 { type: "separator" },
10 { label: "Logout", icon: LogOut, variant: "destructive", onSelect: () => {} },
11 ];
12</script>
13
14<div class="flex flex-wrap gap-4">
15 <DropdownMenu triggerText="Default" triggerVariant="default" items={items} />
16 <DropdownMenu triggerText="Secondary" triggerVariant="secondary" items={items} />
17 <DropdownMenu triggerText="Outline" triggerVariant="outline" items={items} />
18 <DropdownMenu triggerText="Ghost" triggerVariant="ghost" items={items} />
19 <DropdownMenu triggerText="Destructive" triggerVariant="destructive" items={items} />
20</div>Sizes
Available trigger button sizes
Code Svelte
1
2<script lang="ts">
3 import { DropdownMenu } from "@kareyes/aether";
4
5 const items = [
6 { label: "Profile", onSelect: () => {} },
7 { label: "Settings", onSelect: () => {} },
8 ];
9</script>
10
11<div class="flex flex-wrap items-center gap-4">
12 <DropdownMenu triggerText="Small" triggerSize="sm" items={items} />
13 <DropdownMenu triggerText="Default" triggerSize="default" items={items} />
14 <DropdownMenu triggerText="Large" triggerSize="lg" items={items} />
15</div>Content Alignment
Control menu content alignment
Code Svelte
1
2<script lang="ts">
3 import { DropdownMenu } from "@kareyes/aether";
4
5 const items = [
6 { label: "Profile", onSelect: () => {} },
7 { label: "Settings", onSelect: () => {} },
8 ];
9</script>
10
11<div class="flex flex-wrap gap-4">
12 <DropdownMenu triggerText="Align Start" align="start" items={items} />
13 <DropdownMenu triggerText="Align Center" align="center" items={items} />
14 <DropdownMenu triggerText="Align End" align="end" items={items} />
15</div>With Submenu
Nested menus for hierarchical navigation
Code Svelte
1
2<script lang="ts">
3 import { DropdownMenu } from "@kareyes/aether";
4 import { User, Share2, Copy, Download, Mail, MessageSquare, Settings } from "@kareyes/aether/icons";
5
6 const shareMenuItems = [
7 { label: "Profile", icon: User, onSelect: () => console.log("Profile") },
8 {
9 type: "submenu" as const,
10 label: "Share",
11 icon: Share2,
12 items: [
13 { label: "Copy Link", icon: Copy, onSelect: () => console.log("Copy Link") },
14 { label: "Download", icon: Download, onSelect: () => console.log("Download") },
15 { type: "separator" as const },
16 { label: "Email", icon: Mail, onSelect: () => console.log("Email") },
17 { label: "Message", icon: MessageSquare, onSelect: () => console.log("Message") },
18 ]
19 },
20 { type: "separator" as const },
21 { label: "Settings", icon: Settings, onSelect: () => console.log("Settings") },
22 ];
23
24 const fileMenuWithSubmenu = [
25 { label: "New File", icon: Plus, shortcut: "⌘N", onSelect: () => console.log("New file") },
26 {
27 type: "submenu" as const,
28 label: "New From Template",
29 icon: FileText,
30 items: [
31 { label: "Text Document", icon: FileText, onSelect: () => console.log("Text") },
32 { label: "Image", icon: Image, onSelect: () => console.log("Image") },
33 { label: "Video", icon: Video, onSelect: () => console.log("Video") },
34 { label: "Audio", icon: Music, onSelect: () => console.log("Audio") },
35 ]
36 },
37 { type: "separator" as const },
38 { label: "Upload", icon: Upload, shortcut: "⌘U", onSelect: () => console.log("Upload") },
39 { label: "Download", icon: Download, shortcut: "⌘D", onSelect: () => console.log("Download") },
40 { type: "separator" as const },
41 { label: "Delete", icon: Trash2, variant: "destructive" as const, shortcut: "⌘⌫", onSelect: () => console.log("Delete") },
42 ];
43
44 const nestedSubmenuItems = [
45 { label: "Home", onSelect: () => console.log("Home") },
46 {
47 type: "submenu" as const,
48 label: "File",
49 icon: Folder,
50 items: [
51 { label: "New", icon: Plus, shortcut: "⌘N", onSelect: () => console.log("New") },
52 {
53 type: "submenu" as const,
54 label: "Open Recent",
55 items: [
56 { label: "Document 1.txt", icon: File, onSelect: () => console.log("Doc 1") },
57 { label: "Document 2.txt", icon: File, onSelect: () => console.log("Doc 2") },
58 { label: "Document 3.txt", icon: File, onSelect: () => console.log("Doc 3") },
59 ]
60 },
61 { type: "separator" as const },
62 { label: "Save", shortcut: "⌘S", onSelect: () => console.log("Save") },
63 ]
64 },
65 { type: "separator" as const },
66 { label: "Exit", variant: "destructive" as const, onSelect: () => console.log("Exit") },
67 ];
68</script>
69<div class="flex flex-wrap gap-4">
70 <DropdownMenu
71 triggerText="Share Menu"
72 triggerIcon={Share2}
73 items={shareMenuItems}
74 />
75
76 <DropdownMenu
77 triggerText="File Menu"
78 triggerVariant="outline"
79 items={fileMenuWithSubmenu}
80 />
81
82 <DropdownMenu
83 triggerText="Nested Submenus"
84 triggerVariant="secondary"
85 items={nestedSubmenuItems}
86 />
87</div>Custom Trigger
Use the trigger snippet prop to provide custom trigger elements like labels, avatars, or any custom component.
JD
John Doe Code Svelte
1
2<script lang="ts">
3 import { DropdownMenu, DropdownMenuPrimitives } from "@kareyes/aether";
4 import { User, LogOut, Plus, Edit, Copy, Share2, Trash2, Bell, Shield, HelpCircle } from "@kareyes/aether/icons";
5
6 const customTriggerMenuItems = [
7 { label: "New Item", icon: Plus, shortcut: "⌘N", onSelect: () => console.log("New Item") },
8 { label: "Edit", icon: Edit, shortcut: "⌘E", onSelect: () => console.log("Edit") },
9 { label: "Duplicate", icon: Copy, shortcut: "⌘D", onSelect: () => console.log("Duplicate") },
10 { type: "separator" as const },
11 { label: "Share", icon: Share2, onSelect: () => console.log("Share") },
12 { type: "separator" as const },
13 { label: "Delete", icon: Trash2, variant: "destructive" as const, shortcut: "⌘⌫", onSelect: () => console.log("Delete") },
14 ];
15
16 const labelTriggerMenuItems = [
17 { label: "My Profile", icon: User, onSelect: () => console.log("Profile") },
18 { label: "Account Settings", icon: Settings, onSelect: () => console.log("Settings") },
19 { label: "Notifications", icon: Bell, onSelect: () => console.log("Notifications") },
20 { type: "separator" as const },
21 { label: "Privacy & Security", icon: Shield, onSelect: () => console.log("Privacy") },
22 { label: "Help Center", icon: HelpCircle, onSelect: () => console.log("Help") },
23 { type: "separator" as const },
24 { label: "Sign Out", icon: LogOut, variant: "destructive" as const, onSelect: () => console.log("Sign Out") },
25 ];
26</script>
27
28<div class="flex flex-wrap gap-6">
29 <!-- Label as Trigger -->
30 <DropdownMenu items={labelTriggerMenuItems}>
31 {#snippet trigger()}
32 <DropdownMenuPrimitive.Trigger>
33 {#snippet child({ props })}
34 <div {...props} class="cursor-pointer hover:opacity-80 transition-opacity">
35 <div class="flex items-center gap-2">
36 <div class="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center text-white text-sm font-semibold">
37 JD
38 </div>
39 <span class="text-sm font-medium">John Doe</span>
40 <ChevronDown class="size-3 text-muted-foreground" />
41 </div>
42 </div>
43 {/snippet}
44 </DropdownMenuPrimitive.Trigger>
45 {/snippet}
46 </DropdownMenu>
47
48 <!-- Text Label as Trigger -->
49 <DropdownMenu items={customTriggerMenuItems}>
50 {#snippet trigger()}
51 <DropdownMenuPrimitive.Trigger>
52 {#snippet child({ props })}
53 <span
54 {...props}
55 class="text-sm font-medium text-primary hover:underline cursor-pointer"
56 >
57 Actions Menu ▼
58 </span>
59 {/snippet}
60 </DropdownMenuPrimitive.Trigger>
61 {/snippet}
62 </DropdownMenu>
63</div>Features
- Simple API: Define menus with a simple array of items
- Multiple Item Types: Regular items, separators, labels, checkboxes, radio groups
- Groups: Organize items into labeled groups
- Icons & Shortcuts: Add icons and keyboard shortcuts to items
- Variants: Support for different trigger button variants and sizes
- Reactive: Works with Svelte's reactive state
- Flexible: Fallback to custom trigger snippet when needed
Basic Usage
<script lang="ts">
import { DropdownMenu } from "@kareyes/aether";
import User from "@lucide/svelte/icons/user";
import Settings from "@lucide/svelte/icons/settings";
import LogOut from "@lucide/svelte/icons/log-out";
const items = [
{ label: "Profile", icon: User, onSelect: () => console.log("Profile") },
{ label: "Settings", icon: Settings, onSelect: () => console.log("Settings") },
{ type: "separator" },
{ label: "Logout", icon: LogOut, variant: "destructive", onSelect: () => console.log("Logout") },
];
</script>
<DropdownMenu
triggerText="User Menu"
{items}
/>
Item Types
Regular Item
{
label: "Profile",
icon: User,
shortcut: "⌘P",
variant: "default", // or "destructive"
disabled: false,
onSelect: () => console.log("Selected")
}
Separator
{ type: "separator" }
Label
{ type: "label", label: "Section Title" }
Checkbox Item
{
type: "checkbox",
label: "Show Toolbar",
checked: true,
onSelect: () => { checked = !checked }
}
Radio Group
{
type: "radio",
label: "Theme",
value: "light",
items: [
{ label: "Light", value: "light" },
{ label: "Dark", value: "dark" },
{ label: "System", value: "system" }
],
onValueChange: (value) => { theme = value }
}
Grouped Items
const items = [
{
label: "Account",
items: [
{ label: "Profile", icon: User },
{ label: "Settings", icon: Settings }
]
},
{ type: "separator" },
{
items: [
{ label: "Logout", icon: LogOut, variant: "destructive" }
]
}
];
Examples
With Shortcuts
<DropdownMenu
triggerText="File"
items={[
{ label: "New", icon: Plus, shortcut: "⌘N", onSelect: () => {} },
{ label: "Open", icon: FolderOpen, shortcut: "⌘O", onSelect: () => {} },
{ type: "separator" },
{ label: "Save", icon: Save, shortcut: "⌘S", onSelect: () => {} }
]}
/>
Interactive Checkboxes
<script>
let showToolbar = $state(true);
let showSidebar = $state(false);
$: items = [
{ type: "label", label: "View Options" },
{
type: "checkbox",
label: "Show Toolbar",
checked: showToolbar,
onSelect: () => showToolbar = !showToolbar
},
{
type: "checkbox",
label: "Show Sidebar",
checked: showSidebar,
onSelect: () => showSidebar = !showSidebar
}
];
</script>
<DropdownMenu triggerText="View" {items} />
Radio Group for Theme
<script>
let theme = $state("light");
$: items = [
{
type: "radio",
label: "Select Theme",
value: theme,
items: [
{ label: "Light", value: "light" },
{ label: "Dark", value: "dark" },
{ label: "System", value: "system" }
],
onValueChange: (value) => { theme = value }
}
];
</script>
<DropdownMenu triggerText={`Theme: ${theme}`} {items} />
Complex Menu
<script>
let theme = $state("light");
let showToolbar = $state(true);
$: items = [
{
label: "Account",
items: [
{ label: "Profile", icon: User, shortcut: "⌘P" },
{ label: "Settings", icon: Settings, shortcut: "⌘," }
]
},
{ type: "separator" },
{
type: "radio",
label: "Theme",
value: theme,
items: [
{ label: "Light", value: "light" },
{ label: "Dark", value: "dark" }
],
onValueChange: (v) => theme = v
},
{ type: "separator" },
{
label: "View",
items: [
{
type: "checkbox",
label: "Toolbar",
checked: showToolbar,
onSelect: () => showToolbar = !showToolbar
}
]
},
{ type: "separator" },
{
items: [
{ label: "Logout", icon: LogOut, variant: "destructive" }
]
}
];
</script>
<DropdownMenu triggerText="Account" {items} />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
triggerText |
string |
"Open" |
Text for the trigger button |
triggerVariant |
ButtonVariant |
"outline" |
Variant of the trigger button |
triggerSize |
ButtonSize |
"default" |
Size of the trigger button |
triggerIcon |
Component |
undefined |
Icon component for trigger |
triggerClass |
string |
undefined |
Additional classes for trigger |
showChevron |
boolean |
true |
Show chevron icon in trigger |
align |
"start" | "center" | "end" |
"start" |
Content alignment |
side |
"top" | "right" | "bottom" | "left" |
"bottom" |
Content side |
sideOffset |
number |
4 |
Offset from trigger |
contentClass |
string |
undefined |
Additional classes for content |
items |
Array<DropdownItem | DropdownGroup> |
[] |
Menu items |
trigger |
Snippet |
undefined |
Custom trigger snippet |
open |
boolean |
false |
Bindable open state |
Item Structure
DropdownItem
{
type?: "item" | "separator" | "label" | "checkbox";
label?: string;
value?: string;
checked?: boolean;
disabled?: boolean;
onSelect?: () => void;
shortcut?: string;
variant?: "default" | "destructive";
icon?: Component;
}
DropdownGroup
{
label?: string;
items: DropdownItem[];
type?: "radio";
value?: string;
onValueChange?: (value: string) => void;
}
Comparison with Standard Dropdown
Before (Standard)
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="outline">Open</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item>
<User class="size-4" />
Profile
</DropdownMenu.Item>
<DropdownMenu.Item>
<Settings class="size-4" />
Settings
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item>
<LogOut class="size-4" />
Logout
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
After (Simplified)
<DropdownMenu
triggerText="Open"
items={[
{ label: "Profile", icon: User },
{ label: "Settings", icon: Settings },
{ type: "separator" },
{ label: "Logout", icon: LogOut }
]}
/>
Notes
- The simplified component is great for common use cases
- For complex nested submenus, use the standard dropdown menu components
- All items are reactive - use
$:or$derivedto update items based on state - The
onSelectcallback is called when an item is clicked - Checkbox and radio items manage their own state - update external state in
onSelectoronValueChange