Components · Overlays
Dropdown menu
A menu button on the native popover: arrow keys, Home, End and type-ahead, checkbox items, Escape returns focus.
8.1% of the 2,062 sites in The UI Index have a menu (166 sites).
Both previews render the same markup; the HTML tab below is that markup without a framework.
Install
- React
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ dropdown-menu.json - Astro
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ astro/ dropdown-menu.json
Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS and the script.
// Dropdown menu behaviour, shared by the Astro, React and HTML versions (WAI-ARIA menu button). The trigger has
// popovertarget="<menu id>" and aria-haspopup="menu"; the menu is [popover][role=menu] with menuitem buttons.
// Opening (click, Enter, Space or ArrowDown) focuses the first item, ArrowUp the last; Up/Down/Home/End move, a
// letter jumps to the next item starting with it, Escape and Tab close it, focus returns to the trigger. Choosing an
// item dispatches a bubbling `menu-select` event ({ value, label, checked? }) from the menu, then closes it; a
// menuitemcheckbox toggles and stays open.
declare global {
interface Window {
__caseroomMenu?: boolean;
}
}
const items = (menu: HTMLElement) => [...menu.querySelectorAll<HTMLElement>('[role^=menuitem]')].filter((i) => !(i as HTMLButtonElement).disabled && i.getAttribute('aria-disabled') !== 'true');
const triggerOf = (menu: HTMLElement) => document.querySelector<HTMLElement>(`[popovertarget="${CSS.escape(menu.id)}"]`);
const anchored = typeof CSS !== 'undefined' && CSS.supports('anchor-name: --a');
/** Without CSS anchor positioning: under the trigger, flipped up when there is no room below. */
function place(menu: HTMLElement, trigger: HTMLElement) {
if (anchored) return;
const t = trigger.getBoundingClientRect();
const m = menu.getBoundingClientRect();
const end = menu.dataset.align === 'end';
const below = t.bottom + 4 + m.height <= innerHeight || t.top < m.height;
menu.style.position = 'fixed';
menu.style.top = `${below ? t.bottom + 4 : t.top - 4 - m.height}px`;
menu.style.left = `${Math.max(8, Math.min(end ? t.right - m.width : t.left, innerWidth - m.width - 8))}px`;
}
if (typeof window !== 'undefined' && !window.__caseroomMenu) {
window.__caseroomMenu = true;
let openedWith: 'first' | 'last' = 'first';
document.addEventListener(
'toggle',
(e) => {
const menu = e.target as HTMLElement;
if (!(menu instanceof HTMLElement) || menu.getAttribute('role') !== 'menu') return;
const open = (e as ToggleEvent).newState === 'open';
const trigger = triggerOf(menu);
trigger?.setAttribute('aria-expanded', String(open));
if (open) {
if (trigger) place(menu, trigger);
const list = items(menu);
(openedWith === 'last' ? list.at(-1) : list[0])?.focus();
openedWith = 'first';
} else if (menu.contains(document.activeElement) || document.activeElement === document.body) trigger?.focus();
},
true,
);
document.addEventListener('keydown', (e) => {
const el = e.target as HTMLElement;
// On the trigger: ArrowDown/ArrowUp open it.
if (el.matches?.('[aria-haspopup=menu][popovertarget]') && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
const menu = document.getElementById(el.getAttribute('popovertarget') ?? '');
if (!menu) return;
e.preventDefault();
openedWith = e.key === 'ArrowUp' ? 'last' : 'first';
if (!menu.matches(':popover-open')) menu.showPopover();
return;
}
const menu = el.closest?.<HTMLElement>('[role=menu][popover]');
if (!menu) return;
const list = items(menu);
const i = list.indexOf(el);
let next: HTMLElement | undefined;
if (e.key === 'ArrowDown') next = list[(i + 1) % list.length];
else if (e.key === 'ArrowUp') next = list[(i - 1 + list.length) % list.length];
else if (e.key === 'Home') next = list[0];
else if (e.key === 'End') next = list.at(-1);
else if (e.key === 'Tab') {
menu.hidePopover();
return;
} else if (e.key.length === 1 && /\S/.test(e.key)) {
const k = e.key.toLowerCase();
const rest = [...list.slice(i + 1), ...list.slice(0, i + 1)];
next = rest.find((x) => x.textContent?.trim().toLowerCase().startsWith(k));
}
if (next) {
e.preventDefault();
next.focus();
}
});
document.addEventListener('click', (e) => {
const item = (e.target as Element).closest<HTMLElement>('[role^=menuitem]');
const menu = item?.closest<HTMLElement>('[role=menu][popover]');
if (!item || !menu || (item as HTMLButtonElement).disabled) return;
const box = item.getAttribute('role') === 'menuitemcheckbox';
if (box) item.setAttribute('aria-checked', String(item.getAttribute('aria-checked') !== 'true'));
menu.dispatchEvent(
new CustomEvent('menu-select', {
bubbles: true,
detail: { value: item.dataset.value ?? item.textContent?.trim(), label: item.textContent?.trim(), ...(box ? { checked: item.getAttribute('aria-checked') === 'true' } : {}) },
}),
);
if (!box) menu.hidePopover();
});
}
export {};import { useEffect, useRef } from 'react';
import { cn } from '@/lib/utils';
import { buttonVariants, type ButtonVariants } from '../button/button.variants';
import './dropdown-menu.client';
import { CHECK_PATH, CHEVRON_PATH, menu, type MenuEntry } from './dropdown-menu.variants';
export type { MenuEntry };
export interface DropdownMenuProps {
/** Unique on the page: the menu's id. */
id: string;
/** The trigger's text and the menu's accessible name. */
label: string;
entries: MenuEntry[];
/** Line the menu up with the trigger's start or end. */
align?: 'start' | 'end';
variant?: ButtonVariants['variant'];
size?: ButtonVariants['size'];
disabled?: boolean;
/** Called with the chosen item (and `checked` for a checkbox item). */
onSelect?: (detail: { value?: string; label?: string; checked?: boolean }) => void;
className?: string;
}
/** A menu button (WAI-ARIA) on the native popover. Keyboard: Enter, Space and the arrows open it; arrows, Home, End
* and a letter move; Escape and Tab close it and focus returns to the trigger. */
export function DropdownMenu({ id, label, entries, align = 'start', variant = 'outline', size, disabled, onSelect, className }: DropdownMenuProps) {
const lead = entries.some((e) => e.type === 'checkbox');
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el || !onSelect) return;
const on = (e: Event) => onSelect((e as CustomEvent).detail);
el.addEventListener('menu-select', on);
return () => el.removeEventListener('menu-select', on);
}, [onSelect]);
return (
<div className={cn(menu.root, className)}>
<button type="button" popoverTarget={id} aria-haspopup="menu" aria-expanded="false" disabled={disabled} className={cn(buttonVariants({ variant, size }), 'group')} style={{ anchorName: `--${id}` } as React.CSSProperties}>
{label}
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={menu.chevron}><path d={CHEVRON_PATH} /></svg>
</button>
<div ref={ref} id={id} popover="auto" role="menu" aria-label={label} data-align={align} className={menu.content} style={{ positionAnchor: `--${id}` } as React.CSSProperties}>
{entries.map((e, k) =>
e.type === 'separator' ? (
<div key={k} role="separator" className={menu.separator} />
) : e.type === 'label' ? (
<div key={k} role="presentation" className={cn(menu.label, lead && 'pl-8')}>{e.label}</div>
) : e.type === 'checkbox' ? (
<button key={k} type="button" role="menuitemcheckbox" aria-checked={e.checked ? 'true' : 'false'} tabIndex={-1} disabled={e.disabled} data-value={e.value} className={cn(menu.item, 'group')}>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={menu.check}><path d={CHECK_PATH} /></svg>
{e.label}
</button>
) : e.href ? (
<a key={k} href={e.href} role="menuitem" tabIndex={-1} className={cn(menu.item, e.destructive && menu.destructive)}>
{lead && <span aria-hidden="true" className={menu.lead} />}
{e.label}
{e.shortcut && <kbd className={menu.shortcut}>{e.shortcut}</kbd>}
</a>
) : (
<button key={k} type="button" role="menuitem" tabIndex={-1} disabled={e.disabled} data-value={e.value} className={cn(menu.item, e.destructive && menu.destructive)}>
{lead && <span aria-hidden="true" className={menu.lead} />}
{e.label}
{e.shortcut && <kbd className={menu.shortcut}>{e.shortcut}</kbd>}
</button>
),
)}
</div>
</div>
);
}/** A menu on the native popover (light dismiss, Escape, top layer), placed under its trigger with CSS anchor
* positioning, and by script where that is missing. It fades in; with reduced motion it only appears. */
export const menu = {
root: 'inline-block',
// Under the trigger, flipped when there is no room (CSS anchor positioning; the script places it where that is missing).
content:
'm-0 [inset:auto] supports-[anchor-name:--a]:mt-1 supports-[anchor-name:--a]:[top:anchor(bottom)] supports-[anchor-name:--a]:[left:anchor(left)] supports-[anchor-name:--a]:[position-try-fallbacks:flip-block,flip-inline] supports-[anchor-name:--a]:data-[align=end]:[left:auto] supports-[anchor-name:--a]:data-[align=end]:[right:anchor(right)] min-w-52 rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg dark:shadow-[0_12px_32px_rgb(0_0_0/0.65)] opacity-0 transition-[opacity,display,overlay] transition-discrete duration-100 open:opacity-100 starting:open:opacity-0',
item: 'flex h-8 w-full cursor-pointer items-center gap-2 rounded-md px-2 text-left text-sm whitespace-nowrap outline-none transition-colors select-none hover:bg-accent focus-visible:bg-accent focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring active:bg-pressed disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0',
destructive: 'text-destructive hover:bg-destructive/10 focus-visible:bg-destructive/10',
// When the menu has checkbox items, every row keeps a 16px leading column, so all labels share one edge.
lead: 'size-4 shrink-0',
check: 'invisible size-4 shrink-0 group-aria-checked:visible',
shortcut: 'ml-auto pl-4 font-sans text-[13px] text-muted-foreground tabular-nums',
label: 'px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground',
separator: '-mx-1 my-1 h-px bg-border',
chevron: 'size-3.5 text-muted-foreground transition-transform duration-150 group-aria-expanded:rotate-180',
};
export type MenuEntry =
| { type?: 'item'; label: string; shortcut?: string; href?: string; destructive?: boolean; disabled?: boolean; value?: string }
| { type: 'checkbox'; label: string; checked?: boolean; disabled?: boolean; value?: string }
| { type: 'label'; label: string }
| { type: 'separator' };
export const CHECK_PATH = 'M3.5 8.5l3 3 6-7';
export const CHEVRON_PATH = 'M4 6l4 4 4-4';---
import { cn } from '@/lib/utils';
import { buttonVariants, type ButtonVariants } from '../button/button.variants';
import { CHECK_PATH, CHEVRON_PATH, menu, type MenuEntry } from './dropdown-menu.variants';
interface Props {
/** Unique on the page: the menu's id. */
id: string;
/** The trigger's text and the menu's accessible name. */
label: string;
entries: MenuEntry[];
/** Line the menu up with the trigger's start or end. */
align?: 'start' | 'end';
variant?: ButtonVariants['variant'];
size?: ButtonVariants['size'];
disabled?: boolean;
class?: string;
}
const { id, label, entries, align = 'start', variant = 'outline', size, disabled, class: className } = Astro.props;
const lead = entries.some((e) => e.type === 'checkbox');
---
<div class={cn(menu.root, className)}>
<button type="button" popovertarget={id} aria-haspopup="menu" aria-expanded="false" disabled={disabled} class={cn(buttonVariants({ variant, size }), 'group')} style={`anchor-name: --${id}`}>
{label}
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={menu.chevron}><path d={CHEVRON_PATH} /></svg>
</button>
<div id={id} popover="auto" role="menu" aria-label={label} data-align={align} class={menu.content} style={`position-anchor: --${id}`}>
{
entries.map((e) =>
e.type === 'separator' ? (
<div role="separator" class={menu.separator} />
) : e.type === 'label' ? (
<div role="presentation" class={cn(menu.label, lead && 'pl-8')}>{e.label}</div>
) : e.type === 'checkbox' ? (
<button type="button" role="menuitemcheckbox" aria-checked={e.checked ? 'true' : 'false'} tabindex="-1" disabled={e.disabled} data-value={e.value} class={cn(menu.item, 'group')}>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={menu.check}><path d={CHECK_PATH} /></svg>
{e.label}
</button>
) : e.href ? (
<a href={e.href} role="menuitem" tabindex="-1" class={cn(menu.item, e.destructive && menu.destructive)}>
{lead && <span aria-hidden="true" class={menu.lead} />}
{e.label}
{e.shortcut && <kbd class={menu.shortcut}>{e.shortcut}</kbd>}
</a>
) : (
<button type="button" role="menuitem" tabindex="-1" disabled={e.disabled} data-value={e.value} class={cn(menu.item, e.destructive && menu.destructive)}>
{lead && <span aria-hidden="true" class={menu.lead} />}
{e.label}
{e.shortcut && <kbd class={menu.shortcut}>{e.shortcut}</kbd>}
</button>
),
)
}
</div>
</div>
<script>
import './dropdown-menu.client';
</script>// Dropdown menu behaviour, shared by the Astro, React and HTML versions (WAI-ARIA menu button). The trigger has
// popovertarget="<menu id>" and aria-haspopup="menu"; the menu is [popover][role=menu] with menuitem buttons.
// Opening (click, Enter, Space or ArrowDown) focuses the first item, ArrowUp the last; Up/Down/Home/End move, a
// letter jumps to the next item starting with it, Escape and Tab close it, focus returns to the trigger. Choosing an
// item dispatches a bubbling `menu-select` event ({ value, label, checked? }) from the menu, then closes it; a
// menuitemcheckbox toggles and stays open.
declare global {
interface Window {
__caseroomMenu?: boolean;
}
}
const items = (menu: HTMLElement) => [...menu.querySelectorAll<HTMLElement>('[role^=menuitem]')].filter((i) => !(i as HTMLButtonElement).disabled && i.getAttribute('aria-disabled') !== 'true');
const triggerOf = (menu: HTMLElement) => document.querySelector<HTMLElement>(`[popovertarget="${CSS.escape(menu.id)}"]`);
const anchored = typeof CSS !== 'undefined' && CSS.supports('anchor-name: --a');
/** Without CSS anchor positioning: under the trigger, flipped up when there is no room below. */
function place(menu: HTMLElement, trigger: HTMLElement) {
if (anchored) return;
const t = trigger.getBoundingClientRect();
const m = menu.getBoundingClientRect();
const end = menu.dataset.align === 'end';
const below = t.bottom + 4 + m.height <= innerHeight || t.top < m.height;
menu.style.position = 'fixed';
menu.style.top = `${below ? t.bottom + 4 : t.top - 4 - m.height}px`;
menu.style.left = `${Math.max(8, Math.min(end ? t.right - m.width : t.left, innerWidth - m.width - 8))}px`;
}
if (typeof window !== 'undefined' && !window.__caseroomMenu) {
window.__caseroomMenu = true;
let openedWith: 'first' | 'last' = 'first';
document.addEventListener(
'toggle',
(e) => {
const menu = e.target as HTMLElement;
if (!(menu instanceof HTMLElement) || menu.getAttribute('role') !== 'menu') return;
const open = (e as ToggleEvent).newState === 'open';
const trigger = triggerOf(menu);
trigger?.setAttribute('aria-expanded', String(open));
if (open) {
if (trigger) place(menu, trigger);
const list = items(menu);
(openedWith === 'last' ? list.at(-1) : list[0])?.focus();
openedWith = 'first';
} else if (menu.contains(document.activeElement) || document.activeElement === document.body) trigger?.focus();
},
true,
);
document.addEventListener('keydown', (e) => {
const el = e.target as HTMLElement;
// On the trigger: ArrowDown/ArrowUp open it.
if (el.matches?.('[aria-haspopup=menu][popovertarget]') && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
const menu = document.getElementById(el.getAttribute('popovertarget') ?? '');
if (!menu) return;
e.preventDefault();
openedWith = e.key === 'ArrowUp' ? 'last' : 'first';
if (!menu.matches(':popover-open')) menu.showPopover();
return;
}
const menu = el.closest?.<HTMLElement>('[role=menu][popover]');
if (!menu) return;
const list = items(menu);
const i = list.indexOf(el);
let next: HTMLElement | undefined;
if (e.key === 'ArrowDown') next = list[(i + 1) % list.length];
else if (e.key === 'ArrowUp') next = list[(i - 1 + list.length) % list.length];
else if (e.key === 'Home') next = list[0];
else if (e.key === 'End') next = list.at(-1);
else if (e.key === 'Tab') {
menu.hidePopover();
return;
} else if (e.key.length === 1 && /\S/.test(e.key)) {
const k = e.key.toLowerCase();
const rest = [...list.slice(i + 1), ...list.slice(0, i + 1)];
next = rest.find((x) => x.textContent?.trim().toLowerCase().startsWith(k));
}
if (next) {
e.preventDefault();
next.focus();
}
});
document.addEventListener('click', (e) => {
const item = (e.target as Element).closest<HTMLElement>('[role^=menuitem]');
const menu = item?.closest<HTMLElement>('[role=menu][popover]');
if (!item || !menu || (item as HTMLButtonElement).disabled) return;
const box = item.getAttribute('role') === 'menuitemcheckbox';
if (box) item.setAttribute('aria-checked', String(item.getAttribute('aria-checked') !== 'true'));
menu.dispatchEvent(
new CustomEvent('menu-select', {
bubbles: true,
detail: { value: item.dataset.value ?? item.textContent?.trim(), label: item.textContent?.trim(), ...(box ? { checked: item.getAttribute('aria-checked') === 'true' } : {}) },
}),
);
if (!box) menu.hidePopover();
});
}
export {};/** A menu on the native popover (light dismiss, Escape, top layer), placed under its trigger with CSS anchor
* positioning, and by script where that is missing. It fades in; with reduced motion it only appears. */
export const menu = {
root: 'inline-block',
// Under the trigger, flipped when there is no room (CSS anchor positioning; the script places it where that is missing).
content:
'm-0 [inset:auto] supports-[anchor-name:--a]:mt-1 supports-[anchor-name:--a]:[top:anchor(bottom)] supports-[anchor-name:--a]:[left:anchor(left)] supports-[anchor-name:--a]:[position-try-fallbacks:flip-block,flip-inline] supports-[anchor-name:--a]:data-[align=end]:[left:auto] supports-[anchor-name:--a]:data-[align=end]:[right:anchor(right)] min-w-52 rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg dark:shadow-[0_12px_32px_rgb(0_0_0/0.65)] opacity-0 transition-[opacity,display,overlay] transition-discrete duration-100 open:opacity-100 starting:open:opacity-0',
item: 'flex h-8 w-full cursor-pointer items-center gap-2 rounded-md px-2 text-left text-sm whitespace-nowrap outline-none transition-colors select-none hover:bg-accent focus-visible:bg-accent focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring active:bg-pressed disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0',
destructive: 'text-destructive hover:bg-destructive/10 focus-visible:bg-destructive/10',
// When the menu has checkbox items, every row keeps a 16px leading column, so all labels share one edge.
lead: 'size-4 shrink-0',
check: 'invisible size-4 shrink-0 group-aria-checked:visible',
shortcut: 'ml-auto pl-4 font-sans text-[13px] text-muted-foreground tabular-nums',
label: 'px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground',
separator: '-mx-1 my-1 h-px bg-border',
chevron: 'size-3.5 text-muted-foreground transition-transform duration-150 group-aria-expanded:rotate-180',
};
export type MenuEntry =
| { type?: 'item'; label: string; shortcut?: string; href?: string; destructive?: boolean; disabled?: boolean; value?: string }
| { type: 'checkbox'; label: string; checked?: boolean; disabled?: boolean; value?: string }
| { type: 'label'; label: string }
| { type: 'separator' };
export const CHECK_PATH = 'M3.5 8.5l3 3 6-7';
export const CHEVRON_PATH = 'M4 6l4 4 4-4';<div class="flex flex-wrap items-start gap-3">
<div class="inline-block">
<button type="button" popovertarget="ex-menu" aria-haspopup="menu" aria-expanded="false" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-pressed h-9 px-4 group" style="anchor-name: --ex-menu">
Project
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="size-3.5 text-muted-foreground transition-transform duration-150 group-aria-expanded:rotate-180"><path d="M4 6l4 4 4-4"></path></svg>
</button>
<div id="ex-menu" popover="auto" role="menu" aria-label="Project" data-align="start" class="m-0 [inset:auto] supports-[anchor-name:--a]:mt-1 supports-[anchor-name:--a]:[top:anchor(bottom)] supports-[anchor-name:--a]:[left:anchor(left)] supports-[anchor-name:--a]:[position-try-fallbacks:flip-block,flip-inline] supports-[anchor-name:--a]:data-[align=end]:[left:auto] supports-[anchor-name:--a]:data-[align=end]:[right:anchor(right)] min-w-52 rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg dark:shadow-[0_12px_32px_rgb(0_0_0/0.65)] opacity-0 transition-[opacity,display,overlay] transition-discrete duration-100 open:opacity-100 starting:open:opacity-0" style="position-anchor: --ex-menu">
<div role="presentation" class="px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground pl-8">Northwind site</div>
<button type="button" role="menuitem" tabindex="-1" data-value="open" class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-md px-2 text-left text-sm whitespace-nowrap outline-none transition-colors select-none hover:bg-accent focus-visible:bg-accent focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring active:bg-pressed disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0">
<span aria-hidden="true" class="size-4 shrink-0"></span>
Open
<kbd class="ml-auto pl-4 font-sans text-[13px] text-muted-foreground tabular-nums">⌘O</kbd>
</button>
<button type="button" role="menuitem" tabindex="-1" data-value="duplicate" class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-md px-2 text-left text-sm whitespace-nowrap outline-none transition-colors select-none hover:bg-accent focus-visible:bg-accent focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring active:bg-pressed disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0">
<span aria-hidden="true" class="size-4 shrink-0"></span>
Duplicate
<kbd class="ml-auto pl-4 font-sans text-[13px] text-muted-foreground tabular-nums">⌘D</kbd>
</button>
<button type="button" role="menuitem" tabindex="-1" disabled data-value="move" class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-md px-2 text-left text-sm whitespace-nowrap outline-none transition-colors select-none hover:bg-accent focus-visible:bg-accent focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring active:bg-pressed disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0">
<span aria-hidden="true" class="size-4 shrink-0"></span>
Move to team
</button>
<div role="separator" class="-mx-1 my-1 h-px bg-border"></div>
<button type="button" role="menuitemcheckbox" aria-checked="true" tabindex="-1" data-value="sidebar" class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-md px-2 text-left text-sm whitespace-nowrap outline-none transition-colors select-none hover:bg-accent focus-visible:bg-accent focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring active:bg-pressed disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 group">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="invisible size-4 shrink-0 group-aria-checked:visible"><path d="M3.5 8.5l3 3 6-7"></path></svg>
Show in sidebar
</button>
<div role="separator" class="-mx-1 my-1 h-px bg-border"></div>
<button type="button" role="menuitem" tabindex="-1" data-value="delete" class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-md px-2 text-left text-sm whitespace-nowrap outline-none transition-colors select-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring active:bg-pressed disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 text-destructive hover:bg-destructive/10 focus-visible:bg-destructive/10">
<span aria-hidden="true" class="size-4 shrink-0"></span>
Delete
</button>
</div>
</div>
<div class="inline-block">
<button type="button" popovertarget="ex-menu-off" aria-haspopup="menu" aria-expanded="false" disabled class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-pressed h-9 px-4 group" style="anchor-name: --ex-menu-off">
Export
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="size-3.5 text-muted-foreground transition-transform duration-150 group-aria-expanded:rotate-180"><path d="M4 6l4 4 4-4"></path></svg>
</button>
<div id="ex-menu-off" popover="auto" role="menu" aria-label="Export" data-align="start" class="m-0 [inset:auto] supports-[anchor-name:--a]:mt-1 supports-[anchor-name:--a]:[top:anchor(bottom)] supports-[anchor-name:--a]:[left:anchor(left)] supports-[anchor-name:--a]:[position-try-fallbacks:flip-block,flip-inline] supports-[anchor-name:--a]:data-[align=end]:[left:auto] supports-[anchor-name:--a]:data-[align=end]:[right:anchor(right)] min-w-52 rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg dark:shadow-[0_12px_32px_rgb(0_0_0/0.65)] opacity-0 transition-[opacity,display,overlay] transition-discrete duration-100 open:opacity-100 starting:open:opacity-0" style="position-anchor: --ex-menu-off">
<button type="button" role="menuitem" tabindex="-1" class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-md px-2 text-left text-sm whitespace-nowrap outline-none transition-colors select-none hover:bg-accent focus-visible:bg-accent focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring active:bg-pressed disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0">
CSV
</button>
</div>
</div>
</div><script type="module" src="/caseroom/r/html/dropdown-menu.js"></script>