# Context Menu

Contextual menu opened by right-click, long-press, or the keyboard, with submenus and checkbox items.

- Package: `@mielui/svelte`
- Component version: `1.1.1`
- Depends on Mielui components: _internal/utils, button
- Shared utilities: components/_internal/button-attributes, transition, components/_internal/surface, hugeicons-icon, utils.cn, utils.createContext, utils.travelingHighlight

## Install

~~~~sh
pnpm dlx @mielui/svelte add context-menu
~~~~

## API

This reference is generated at build time from the component manifest, public `index.ts`, and documentation examples below. Changes to those source files are reflected here in the published Markdown. Standard Svelte and HTML attributes accepted by the exported prop types are supported.


### CheckboxItem

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| callback | (() => void) \| undefined | - | No | No | Called when the item is chosen. |
| value | string \| (string & string[]) | - | Yes | No | Value reported for the item. |
| inset | boolean \| undefined | false | No | No | Indents the item so it lines up with items that have an indicator. |
| checked | boolean \| undefined | false | No | Yes | Whether the item is checked. Bindable. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| onkeydown | ((event: KeyboardEvent) => void) \| undefined | - | No | No | Called on key down, before the button handles the key itself. |
| onclick | ((event: MouseEvent) => void) \| undefined | - | No | No | Called when the button is activated. Loading and disabled buttons do not call it. |
| status | ButtonStatus \| undefined | - | No | No | Controlled visual state. Loading remains focusable and refuses activation. |
| disabled | boolean \| undefined | false | No | No | Prevents activation. A disabled link loses its destination and is skipped by Tab. |
| element | HTMLButtonElement \| HTMLAnchorElement \| undefined | - | No | Yes | Bindable reference to the rendered DOM element. Type is the union of the two possible element types -- narrow at the use site: |
| loading | boolean \| undefined | - | No | No | Convenience alias for `status="loading"`. |
| labels | ButtonLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Content

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| surface | "solid" \| "glass" \| undefined | - | No | No | Surface treatment. Omit it to inherit `--mielui-surface` from the theme. |



### Item

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| callback | (() => void) \| undefined | - | No | No | Called when the item is chosen. |
| inset | boolean \| undefined | false | No | No | Indents the item so it lines up with items that have an indicator. |
| variant | "destructive" \| undefined | - | No | No | Marks an action that deletes or cannot be undone. Omit it for a regular item. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| onkeydown | ((event: KeyboardEvent) => void) \| undefined | - | No | No | Called on key down, before the button handles the key itself. |
| onclick | ((event: MouseEvent) => void) \| undefined | - | No | No | Called when the button is activated. Loading and disabled buttons do not call it. |
| status | ButtonStatus \| undefined | - | No | No | Controlled visual state. Loading remains focusable and refuses activation. |
| disabled | boolean \| undefined | false | No | No | Prevents activation. A disabled link loses its destination and is skipped by Tab. |
| element | HTMLButtonElement \| HTMLAnchorElement \| undefined | - | No | Yes | Bindable reference to the rendered DOM element. Type is the union of the two possible element types -- narrow at the use site: |
| loading | boolean \| undefined | - | No | No | Convenience alias for `status="loading"`. |
| labels | ButtonLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Root

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| open | boolean \| undefined | false | No | Yes | Whether it is open. Bind it to control the state from outside. |
| onOpenChange | ((open: boolean) => void) \| undefined | - | No | No | Called with the new state whenever it opens or closes. |



### Separator

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Sub

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### SubContent

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| surface | "solid" \| "glass" \| undefined | - | No | No | Surface treatment. Omit it to inherit `--mielui-surface` from the theme. |



### SubTrigger

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| inset | boolean \| undefined | false | No | No | Indents the item so it lines up with items that have an indicator. |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Trigger

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Public types

~~~~ts
import type { VirtualElement } from '@floating-ui/dom';
import type { DefaultProps } from '@mielui/svelte/utils';
import type { Snippet } from 'svelte';
import type { MenuRowButtonProps } from '../_internal/button-attributes';
import Root from './context-menu.svelte';
import CheckboxItem from './context-menu-checkbox-item.svelte';
import Content from './context-menu-content.svelte';
import Item from './context-menu-item.svelte';
import Separator from './context-menu-separator.svelte';
import Sub from './context-menu-sub.svelte';
import SubContent from './context-menu-sub-content.svelte';
import SubTrigger from './context-menu-sub-trigger.svelte';
import Trigger from './context-menu-trigger.svelte';

export type ContextMenuProps = {
    /** Content rendered inside. */
    children?: Snippet;
    /** Whether it is open. Bind it to control the state from outside. */
    open?: boolean;
    /** Called with the new state whenever it opens or closes. */
    onOpenChange?: (open: boolean) => void;
};
export type ContextMenuContentProps = DefaultProps & { surface?: 'solid' | 'glass' };

export type ContextMenuItemProps = {
    /** Called when the item is chosen. */
    callback?: () => void;
    /** Indents the item so it lines up with items that have an indicator. */
    inset?: boolean;
    /** Marks an action that deletes or cannot be undone. Omit it for a regular item. */
    variant?: 'destructive';
} & MenuRowButtonProps;

export type ContextMenuCheckboxItemProps = {
    /** Called when the item is chosen. */
    callback?: () => void;
    /** Value reported for the item. */
    value: string;
    /** Indents the item so it lines up with items that have an indicator. */
    inset?: boolean;
    /** Whether the item is checked. Bindable. */
    checked?: boolean;
} & MenuRowButtonProps;

export type ContextMenuSeparatorProps = DefaultProps;
export type ContextMenuSubContentProps = DefaultProps & { surface?: 'solid' | 'glass' };

export type ContextMenuSubTriggerProps = {
    /** Indents the item so it lines up with items that have an indicator. */
    inset?: boolean;
} & DefaultProps;

export type ContextMenuSubProps = { children?: Snippet };
export type ContextMenuTriggerProps = DefaultProps;

export type ContextMenuState = {
    open: boolean;
    virtualElement?: VirtualElement;
    checkboxItems: Map<string, boolean>;
};

export { CheckboxItem, Content, Item, Root, Separator, Sub, SubContent, SubTrigger, Trigger };
~~~~

## Examples

### Checkbox Items

~~~~svelte
<script lang="ts">
    import * as ContextMenu from '@mielui/svelte/components/context-menu';

    let grid = $state(true);
    let rulers = $state(false);
</script>

<ContextMenu.Root>
    <ContextMenu.Trigger
        class="grid h-32 w-72 place-items-center rounded-[var(--radius-xl)] border border-dashed border-border text-sm text-foreground-muted"
    >
        Right-click the canvas. Grid {grid ? 'on' : 'off'}, rulers {rulers ? 'on' : 'off'}.
    </ContextMenu.Trigger>
    <ContextMenu.Content class="min-w-[12rem]">
        <ContextMenu.CheckboxItem value="grid" bind:checked={grid}>
            Show grid
        </ContextMenu.CheckboxItem>
        <ContextMenu.CheckboxItem value="rulers" bind:checked={rulers}>
            Show rulers
        </ContextMenu.CheckboxItem>
        <ContextMenu.Separator />
        <ContextMenu.Item inset>Reset view</ContextMenu.Item>
    </ContextMenu.Content>
</ContextMenu.Root>
~~~~

### File Row

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as ContextMenu from '@mielui/svelte/components/context-menu';
    import { Input } from '@mielui/svelte/components/input';

    const initialName = 'Release notes.md';
    let name = $state(initialName);
    let draft = $state(initialName);
    let editing = $state(false);
    let message = $state('');

    function rename() {
        draft = name;
        editing = true;
    }

    function save(event: SubmitEvent) {
        event.preventDefault();
        name = draft.trim();
        editing = false;
        message = `Renamed to ${name}.`;
    }
</script>

<div class="flex w-full max-w-sm flex-col gap-3">
    <ContextMenu.Root>
        <ContextMenu.Trigger>
            <div class="rounded-[var(--radius-xl)] border border-border bg-card p-4 text-sm">
                {name}
            </div>
        </ContextMenu.Trigger>
        <ContextMenu.Content>
            <ContextMenu.Item callback={rename}>Rename file</ContextMenu.Item>
        </ContextMenu.Content>
    </ContextMenu.Root>
    {#if editing}
        <form onsubmit={save} class="flex flex-col gap-3">
            <Input label="File name" bind:value={draft} required />
            <Button type="submit" disabled={!draft.trim()}>Save name</Button>
        </form>
    {/if}
    <p class="text-xs text-foreground-muted">Open the file menu to rename it.</p>
    <p role="status" class="text-sm text-foreground-muted">{message}</p>
</div>
~~~~

### Glass

~~~~svelte
<script lang="ts">
    import * as ContextMenu from '@mielui/svelte/components/context-menu';
</script>

<ContextMenu.Root>
    <ContextMenu.Trigger
        class="grid h-32 w-72 place-items-center rounded-[var(--radius-xl)] border border-dashed border-border text-sm text-foreground-muted"
    >
        Right-click or press and hold
    </ContextMenu.Trigger>
    <ContextMenu.Content surface="glass">
        <ContextMenu.Item>Open</ContextMenu.Item>
        <ContextMenu.Item>Duplicate</ContextMenu.Item>
        <ContextMenu.Separator />
        <ContextMenu.Sub>
            <ContextMenu.SubTrigger>Organize</ContextMenu.SubTrigger>
            <ContextMenu.SubContent surface="glass">
                <ContextMenu.Item>Move to folder</ContextMenu.Item>
                <ContextMenu.Item>Add tag</ContextMenu.Item>
            </ContextMenu.SubContent>
        </ContextMenu.Sub>
    </ContextMenu.Content>
</ContextMenu.Root>
~~~~

### Hero

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as ContextMenu from '@mielui/svelte/components/context-menu';

    const initialName = 'Release notes.md';
    const actions = ['Open', 'Duplicate', 'Delete'] as const;
    let fileName = $state(initialName);
    let copies = $state(1);
    let deleted = $state(false);
    let message = $state('');

    function apply(action: string) {
        if (action === 'Open') {
            message = `Opened ${fileName} in this preview.`;
        } else if (action === 'Rename') {
            fileName = `Updated ${initialName.toLowerCase()}`;
            message = 'Name updated.';
        } else if (action === 'Duplicate') {
            copies += 1;
            message = `Created copy ${copies}.`;
        } else {
            deleted = true;
            message = 'Removed from this preview. Restore it to try again.';
        }
    }

    function restore() {
        deleted = false;
        copies = 1;
        fileName = initialName;
        message = '';
    }
</script>

<div class="flex w-full max-w-sm flex-col gap-3">
    {#if deleted}
        <Button variant="secondary" onclick={restore}>Restore item</Button>
    {:else}
        <ContextMenu.Root>
            <ContextMenu.Trigger>
                <div
                    class="flex min-h-28 flex-col items-center justify-center gap-2 rounded-[var(--radius-xl)] border border-dashed border-border p-5 text-center"
                >
                    <span class="text-sm">{fileName}</span>
                    <span class="text-xs text-foreground-muted">
                        {`${copies} ${copies === 1 ? 'item' : 'items'}`}
                    </span>
                    <span class="text-xs text-foreground-muted">Right-click or press and hold</span>
                </div>
            </ContextMenu.Trigger>
            <ContextMenu.Content>
                {#each actions as action (action)}
                    <ContextMenu.Item
                        variant={action === 'Delete' ? 'destructive' : undefined}
                        callback={() => apply(action)}
                    >
                        {action}
                    </ContextMenu.Item>
                {/each}
                <ContextMenu.Separator />
                <ContextMenu.Sub>
                    <ContextMenu.SubTrigger>Organize</ContextMenu.SubTrigger>
                    <ContextMenu.SubContent>
                        <ContextMenu.Item callback={() => apply('Rename')}>
                            Rename to updated draft
                        </ContextMenu.Item>
                        <ContextMenu.Item callback={restore}>Restore original</ContextMenu.Item>
                    </ContextMenu.SubContent>
                </ContextMenu.Sub>
            </ContextMenu.Content>
        </ContextMenu.Root>
    {/if}
    <p role="status" class="text-sm text-foreground-muted">{message}</p>
</div>
~~~~

### Image

~~~~svelte
<script lang="ts">
    import * as ContextMenu from '@mielui/svelte/components/context-menu';

    let rotation = $state(0);
    let message = $state('');

    function rotate() {
        rotation = (rotation + 90) % 360;
        message = `Rotated ${rotation} degrees.`;
    }

    function reset() {
        rotation = 0;
        message = 'Original orientation restored.';
    }

    async function copyLink() {
        try {
            await navigator.clipboard.writeText(
                `${window.location.origin}/docs/components/context-menu#image`
            );
            message = 'Preview link copied.';
        } catch {
            message = 'Clipboard access is unavailable.';
        }
    }
</script>

<div class="flex flex-col items-center gap-3">
    <ContextMenu.Root>
        <ContextMenu.Trigger>
            <svg
                role="img"
                aria-label="Geometric illustration"
                viewBox="0 0 160 120"
                class="h-32 w-44 rounded-[var(--radius-xl)] border border-border bg-secondary p-4"
            >
                <g transform={`rotate(${rotation} 80 60)`}>
                    <circle cx="100" cy="35" r="16" fill="var(--color-primary)" />
                    <path d="M10 100L60 35L110 100Z" fill="var(--color-foreground)" opacity="0.6" />
                    <path d="M65 100L110 60L150 100Z" fill="var(--color-primary)" />
                </g>
            </svg>
        </ContextMenu.Trigger>
        <ContextMenu.Content>
            <ContextMenu.Item callback={rotate}>Rotate clockwise</ContextMenu.Item>
            <ContextMenu.Item callback={reset}>Reset orientation</ContextMenu.Item>
            <ContextMenu.Separator />
            <ContextMenu.Item callback={copyLink}>Copy preview link</ContextMenu.Item>
        </ContextMenu.Content>
    </ContextMenu.Root>
    <p class="text-xs text-foreground-muted">Right-click or press and hold the illustration.</p>
    <p role="status" class="text-sm text-foreground-muted">{message}</p>
</div>
~~~~

### Task Card

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as ContextMenu from '@mielui/svelte/components/context-menu';

    const initialName = 'Release checklist';
    const actions = ['Open', 'Mark complete', 'Duplicate', 'Delete'] as const;
    let fileName = $state(initialName);
    let copies = $state(1);
    let deleted = $state(false);
    let complete = $state(false);
    let message = $state('');

    function apply(action: string) {
        if (action === 'Open') {
            message = `Opened ${fileName} in this preview.`;
        } else if (action === 'Rename') {
            fileName = `Updated ${initialName.toLowerCase()}`;
            message = 'Name updated.';
        } else if (action === 'Duplicate') {
            copies += 1;
            message = `Created copy ${copies}.`;
        } else if (action === 'Mark complete') {
            complete = !complete;
            message = complete ? 'Task completed.' : 'Task reopened.';
        } else {
            deleted = true;
            message = 'Removed from this preview. Restore it to try again.';
        }
    }

    function restore() {
        deleted = false;
        copies = 1;
        fileName = initialName;
        complete = false;
        message = '';
    }
</script>

<div class="flex w-full max-w-sm flex-col gap-3">
    {#if deleted}
        <Button variant="secondary" onclick={restore}>Restore item</Button>
    {:else}
        <ContextMenu.Root>
            <ContextMenu.Trigger>
                <div
                    class="flex min-h-28 flex-col items-center justify-center gap-2 rounded-[var(--radius-xl)] border border-dashed border-border p-5 text-center"
                >
                    <span class="text-sm">{fileName}</span>
                    <span class="text-xs text-foreground-muted">
                        {complete ? 'Completed' : `${copies} ${copies === 1 ? 'item' : 'items'}`}
                    </span>
                    <span class="text-xs text-foreground-muted">Right-click or press and hold</span>
                </div>
            </ContextMenu.Trigger>
            <ContextMenu.Content>
                {#each actions as action (action)}
                    <ContextMenu.Item callback={() => apply(action)}>
                        {action === 'Mark complete' && complete ? 'Reopen task' : action}
                    </ContextMenu.Item>
                {/each}
            </ContextMenu.Content>
        </ContextMenu.Root>
    {/if}
    <p role="status" class="text-sm text-foreground-muted">{message}</p>
</div>
~~~~

For the rendered reference, visit [/docs/components/context-menu](/docs/components/context-menu).
