Context Menu
Displays contextual options and actions triggered by right-click.
<script lang="ts">
import { ContextMenu } from "bits-ui";
import CopySimple from "phosphor-svelte/lib/CopySimple";
import MouseSimple from "phosphor-svelte/lib/MouseSimple";
import PencilSimpleLine from "phosphor-svelte/lib/PencilSimpleLine";
import PlusCircle from "phosphor-svelte/lib/PlusCircle";
import Trash from "phosphor-svelte/lib/Trash";
</script>
<ContextMenu.Root>
<ContextMenu.Trigger
class="rounded-card border-border-input text-muted-foreground flex h-[188px] w-[279px] select-none items-center justify-center border-2 border-dashed bg-transparent font-semibold"
>
<div class="flex flex-col items-center justify-center gap-4 text-center">
<MouseSimple class="size-8" />
Right click me
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content
class="border-muted bg-background shadow-popover w-[229px] rounded-xl border px-1 py-1.5 outline-none focus-visible:outline-none"
>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-medium focus-visible:outline-none"
>
<div class="flex items-center">
<PencilSimpleLine class="text-foreground-alt mr-2 size-5" />
Edit
</div>
<div class="ml-auto flex items-center gap-px">
<kbd
class="rounded-button border-dark-10 bg-background-alt text-muted-foreground shadow-kbd inline-flex size-5 items-center justify-center border text-[13px]"
>
⌘
</kbd>
<kbd
class="rounded-button border-dark-10 bg-background-alt text-muted-foreground shadow-kbd inline-flex size-5 items-center justify-center border text-[11px]"
>
E
</kbd>
</div>
</ContextMenu.Item>
<ContextMenu.Sub>
<ContextMenu.SubTrigger
class="rounded-button data-highlighted:bg-muted data-[state=open]:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-medium focus-visible:outline-none"
>
<div class="flex items-center">
<PlusCircle class="text-foreground-alt mr-2 size-5" />
Add
</div>
<div class="ml-auto flex items-center gap-px">
<kbd
class="rounded-button border-dark-10 bg-background-alt text-muted-foreground shadow-kbd inline-flex size-5 items-center justify-center border text-[13px]"
>
⌘
</kbd>
<kbd
class="rounded-button border-dark-10 bg-background-alt text-muted-foreground shadow-kbd inline-flex size-5 items-center justify-center border text-[11px]"
>
N
</kbd>
</div>
</ContextMenu.SubTrigger>
<ContextMenu.SubContent
class="border-muted bg-background shadow-popover z-100 ring-0! ring-transparent! w-[209px] rounded-xl border px-1 py-1.5"
sideOffset={10}
>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-normal focus-visible:outline-none"
>
Header
</ContextMenu.Item>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-normal focus-visible:outline-none"
>
Paragraph
</ContextMenu.Item>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-normal focus-visible:outline-none"
>
Codeblock
</ContextMenu.Item>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-normal focus-visible:outline-none"
>
List
</ContextMenu.Item>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-normal focus-visible:outline-none"
>
Task
</ContextMenu.Item>
</ContextMenu.SubContent>
</ContextMenu.Sub>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-medium focus-visible:outline-none"
>
<div class="flex items-center">
<CopySimple class="text-foreground-alt mr-2 size-5" />
Duplicate
</div>
<div class="ml-auto flex items-center gap-px">
<kbd
class="rounded-button border-dark-10 bg-background-alt text-muted-foreground shadow-kbd inline-flex size-5 items-center justify-center border text-[13px]"
>
⌘
</kbd>
<kbd
class="rounded-button border-dark-10 bg-background-alt text-muted-foreground shadow-kbd inline-flex size-5 items-center justify-center border text-[11px]"
>
D
</kbd>
</div>
</ContextMenu.Item>
<ContextMenu.Separator class="bg-muted -mx-1 my-1 block h-px" />
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-medium focus-visible:outline-none"
>
<div class="flex items-center">
<Trash class="text-foreground-alt mr-2 size-5" />
Delete
</div>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Root>
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(0 0% 0%);
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(0 0% 100%);
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
Structure
<script lang="ts">
import { ContextMenu } from "bits-ui";
</script>
<ContextMenu.Root>
<ContextMenu.Trigger />
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Group>
<ContextMenu.GroupHeading />
<ContextMenu.Item />
</ContextMenu.Group>
<ContextMenu.Item />
<ContextMenu.CheckboxItem>
{#snippet children({ checked })}
{checked ? "✅" : ""}
{/snippet}
</ContextMenu.CheckboxItem>
<ContextMenu.RadioGroup>
<ContextMenu.GroupHeading />
<ContextMenu.RadioItem>
{#snippet children({ checked })}
{checked ? "✅" : ""}
{/snippet}
</ContextMenu.RadioItem>
</ContextMenu.RadioGroup>
<ContextMenu.Sub>
<ContextMenu.SubTrigger />
<ContextMenu.SubContent />
</ContextMenu.Sub>
<ContextMenu.Separator />
<ContextMenu.Arrow />
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Root>
Reusable Components
If you're planning to use Context Menu in multiple places, you can create a reusable component that wraps the Context Menu component.
This example shows you how to create a Context Menu component that accepts a few custom props that make it more capable.
<script lang="ts">
import type { Snippet } from "svelte";
import { ContextMenu, type WithoutChild } from "bits-ui";
type Props = ContextMenu.Props & {
trigger: Snippet;
items: string[];
contentProps?: WithoutChild<ContextMenu.Content.Props>;
// other component props if needed
};
let {
open = $bindable(false),
children,
trigger,
items,
contentProps,
...restProps
}: Props = $props();
</script>
<ContextMenu.Root bind:open {...restProps}>
<ContextMenu.Trigger>
{@render trigger()}
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content {...contentProps}>
<ContextMenu.Group>
<ContextMenu.GroupHeading>Select an Office</ContextMenu.GroupHeading>
{#each items as item}
<ContextMenu.Item textValue={item}>
{item}
</ContextMenu.Item>
{/each}
</ContextMenu.Group>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Root>
You can then use the CustomContextMenu component like this:
<script lang="ts">
import CustomContextMenu from "./CustomContextMenu.svelte";
</script>
<CustomContextMenu
items={[
"Dunder Mifflin",
"Vance Refrigeration",
"Michael Scott Paper Company",
]}
>
{#snippet triggerArea()}
<div
class="grid size-20 place-items-center rounded-lg border border-dashed p-4"
>
Right-click me
</div>
{/snippet}
</CustomContextMenu>
Alternatively, you can define the snippet(s) separately and pass them as props to the component:
<script lang="ts">
import CustomContextMenu from "./CustomContextMenu.svelte";
</script>
{#snippet triggerArea()}
<div
class="grid size-20 place-items-center rounded-lg border border-dashed p-4"
>
Right-click me
</div>
{/snippet}
<CustomContextMenu
items={[
"Dunder Mifflin",
"Vance Refrigeration",
"Michael Scott Paper Company",
]}
{triggerArea}
/>
Managing Open State
This section covers how to manage the open state of the menu.
Two-Way Binding
Use bind:open for simple, automatic state synchronization:
<script lang="ts">
import { ContextMenu } from "bits-ui";
let isOpen = $state(false);
</script>
<button onclick={() => (isOpen = true)}>Open Context Menu</button>
<ContextMenu.Root bind:open={isOpen}>
<!-- ... -->
</ContextMenu.Root>
Fully Controlled
Use a Function Binding for complete control over the state's reads and writes.
<script lang="ts">
import { ContextMenu } from "bits-ui";
let myOpen = $state(false);
function getOpen() {
return myOpen;
}
function setOpen(newOpen: boolean) {
myOpen = newopen;
}
</script>
<ContextMenu.Root bind:open={getOpen, setOpen}>
<!-- ... -->
</ContextMenu.Root>
Radio Groups
You can combine the ContextMenu.RadioGroup and ContextMenu.RadioItem components to create a radio group within a menu.
<script lang="ts">
import { ContextMenu } from "bits-ui";
const values = ["one", "two", "three"];
let value = $state("one");
</script>
<ContextMenu.RadioGroup bind:value>
{#each values as value}
<ContextMenu.RadioItem {value}>
{#snippet children({ checked })}
{#if checked}
✅
{/if}
{value}
{/snippet}
</ContextMenu.RadioItem>
{/each}
</ContextMenu.RadioGroup>
See the RadioGroup and RadioItem APIs for more information.
Checkbox Items
You can use the ContextMenu.CheckboxItem component to create a menuitemcheckbox element to add checkbox functionality to menu items.
<script lang="ts">
import { ContextMenu } from "bits-ui";
let notifications = $state(true);
</script>
<ContextMenu.CheckboxItem bind:checked={notifications}>
{#snippet children({ checked, indeterminate })}
{#if indeterminate}
-
{:else if checked}
✅
{/if}
Notifications
{/snippet}
</ContextMenu.CheckboxItem>
See the CheckboxItem API for more information.
Checkbox Groups
You can use the ContextMenu.CheckboxGroup component around a set of ContextMenu.CheckboxItem components to create a checkbox group within a menu, where the value prop is an array of the selected values.
<script lang="ts">
import { ContextMenu } from "bits-ui";
let colors = $state<string[]>([]);
</script>
<ContextMenu.CheckboxGroup bind:value={colors}>
<ContextMenu.GroupHeading>Favorite color</ContextMenu.GroupHeading>
<ContextMenu.CheckboxItem value="red">
{#snippet children({ checked })}
{#if checked}
✅
{/if}
Red
{/snippet}
</ContextMenu.CheckboxItem>
<ContextMenu.CheckboxItem value="blue">
{#snippet children({ checked })}
{#if checked}
✅
{/if}
Blue
{/snippet}
</ContextMenu.CheckboxItem>
<ContextMenu.CheckboxItem value="green">
{#snippet children({ checked })}
{#if checked}
✅
{/if}
Green
{/snippet}
</ContextMenu.CheckboxItem>
</ContextMenu.CheckboxGroup>
The value state does not persist between menu open/close cycles. To persist the state, you must store it in a $state variable and pass it to the value prop.
Nested Menus
You can create nested menus using the ContextMenu.Sub component to create complex menu structures.
<script lang="ts">
import { ContextMenu } from "bits-ui";
</script>
<ContextMenu.Content>
<ContextMenu.Item>Item 1</ContextMenu.Item>
<ContextMenu.Item>Item 2</ContextMenu.Item>
<ContextMenu.Sub>
<ContextMenu.SubTrigger>Open Sub Menu</ContextMenu.SubTrigger>
<ContextMenu.SubContent>
<ContextMenu.Item>Sub Item 1</ContextMenu.Item>
<ContextMenu.Item>Sub Item 2</ContextMenu.Item>
</ContextMenu.SubContent>
</ContextMenu.Sub>
</ContextMenu.Content>
Svelte Transitions
You can use the forceMount prop along with the child snippet to forcefully mount the ContextMenu.Content component to use Svelte Transitions or another animation library that requires more control.
<script lang="ts">
import { ContextMenu } from "bits-ui";
import { fly } from "svelte/transition";
</script>
<ContextMenu.Content forceMount>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps}>
<div {...props} transition:fly>
<ContextMenu.Item>Item 1</ContextMenu.Item>
<ContextMenu.Item>Item 2</ContextMenu.Item>
</div>
</div>
{/if}
{/snippet}
</ContextMenu.Content>
Of course, this isn't the prettiest syntax, so it's recommended to create your own reusable content component that handles this logic if you intend to use this approach. For more information on using transitions with Bits UI components, see the Transitions documentation.
<script lang="ts">
import { ContextMenu } from "bits-ui";
import CopySimple from "phosphor-svelte/lib/CopySimple";
import MouseSimple from "phosphor-svelte/lib/MouseSimple";
import PencilSimpleLine from "phosphor-svelte/lib/PencilSimpleLine";
import PlusCircle from "phosphor-svelte/lib/PlusCircle";
import Trash from "phosphor-svelte/lib/Trash";
import { fly } from "svelte/transition";
</script>
<ContextMenu.Root>
<ContextMenu.Trigger
class="rounded-card border-input text-muted-foreground flex h-[188px] w-[279px] select-none items-center justify-center border-2 border-dashed bg-transparent font-semibold"
>
<div class="flex flex-col items-center justify-center gap-4 text-center">
<MouseSimple class="size-8" />
Right click me
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content
class="focus-override border-muted bg-background shadow-popover w-[229px] rounded-xl border px-1 py-1.5 focus-visible:outline-none"
forceMount
>
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps}>
<div {...props} transition:fly={{ duration: 300 }}>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-medium focus-visible:outline-none"
>
<div class="flex items-center">
<PencilSimpleLine class="text-foreground-alt mr-2 size-5" />
Edit
</div>
<div class="ml-auto flex items-center gap-px">
<kbd
class="rounded-button border-dark-10 bg-background-alt text-muted-foreground shadow-kbd inline-flex size-5 items-center justify-center border text-[13px]"
>
⌘
</kbd>
<kbd
class="rounded-button border-dark-10 bg-background-alt text-muted-foreground shadow-kbd inline-flex size-5 items-center justify-center border text-[11px]"
>
E
</kbd>
</div>
</ContextMenu.Item>
<ContextMenu.Sub>
<ContextMenu.SubTrigger
class="rounded-button data-highlighted:bg-muted data-[state=open]:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-medium focus-visible:outline-none"
>
<div class="flex items-center">
<PlusCircle class="text-foreground-alt mr-2 size-5" />
Add
</div>
<div class="ml-auto flex items-center gap-px">
<kbd
class="rounded-button border-dark-10 bg-background-alt text-muted-foreground shadow-kbd inline-flex size-5 items-center justify-center border text-[13px]"
>
⌘
</kbd>
<kbd
class="rounded-button border-dark-10 bg-background-alt text-muted-foreground shadow-kbd inline-flex size-5 items-center justify-center border text-[11px]"
>
N
</kbd>
</div>
</ContextMenu.SubTrigger>
<ContextMenu.SubContent
class="border-muted bg-background shadow-popover z-100 ring-0! ring-transparent! w-[209px] rounded-xl border px-1 py-1.5"
sideOffset={10}
>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-normal focus-visible:outline-none"
>
Header
</ContextMenu.Item>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-normal focus-visible:outline-none"
>
Paragraph
</ContextMenu.Item>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-normal focus-visible:outline-none"
>
Codeblock
</ContextMenu.Item>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-normal focus-visible:outline-none"
>
List
</ContextMenu.Item>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-normal focus-visible:outline-none"
>
Task
</ContextMenu.Item>
</ContextMenu.SubContent>
</ContextMenu.Sub>
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-medium focus-visible:outline-none"
>
<div class="flex items-center">
<CopySimple class="text-foreground-alt mr-2 size-5" />
Duplicate
</div>
<div class="ml-auto flex items-center gap-px">
<kbd
class="rounded-button border-dark-10 bg-background-alt text-muted-foreground shadow-kbd inline-flex size-5 items-center justify-center border text-[13px]"
>
⌘
</kbd>
<kbd
class="rounded-button border-dark-10 bg-background-alt text-muted-foreground shadow-kbd inline-flex size-5 items-center justify-center border text-[11px]"
>
D
</kbd>
</div>
</ContextMenu.Item>
<ContextMenu.Separator class="bg-muted -mx-1 my-1 block h-px" />
<ContextMenu.Item
class="rounded-button data-highlighted:bg-muted flex h-10 select-none items-center py-3 pl-3 pr-1.5 text-sm font-medium focus-visible:outline-none"
>
<div class="flex items-center">
<Trash class="text-foreground-alt mr-2 size-5" />
Delete
</div>
</ContextMenu.Item>
</div>
</div>
{/if}
{/snippet}
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Root>
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(0 0% 0%);
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(0 0% 100%);
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
API Reference
The root component which manages & scopes the state of the context menu.
| Property | Details |
|---|---|
open | |
onOpenChange | |
onOpenChangeComplete | |
dir | |
children |
The element which when right-clicked, opens the context menu.
| Property | Details |
|---|---|
disabled | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-state | |
data-context-menu-trigger |
A component that portals the content of the dropdown menu to the body or a custom target (if provided).
| Property | Details |
|---|---|
to | |
disabled | |
children |
The content displayed when the context menu is open.
| Property | Details |
|---|---|
alignOffset | |
arrowPadding | |
avoidCollisions | |
collisionBoundary | |
collisionPadding | |
sticky | |
hideWhenDetached | |
updatePositionStrategy | |
strategy | |
preventScroll | |
customAnchor | |
onEscapeKeydown | |
escapeKeydownBehavior | |
onInteractOutside | |
onFocusOutside | |
interactOutsideBehavior | |
onOpenAutoFocus | |
onCloseAutoFocus | |
trapFocus | |
preventOverflowTextSelection | |
dir | |
forceMount | |
loop | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-state | |
data-context-menu-content |
| CSS Variable | Details |
|---|---|
--bits-context-menu-content-transform-origin | |
--bits-context-menu-content-available-width | |
--bits-context-menu-content-available-height | |
--bits-context-menu-anchor-width | |
--bits-context-menu-anchor-height |
The content displayed when the context menu is open. (Static/No Floating UI)
| Property | Details |
|---|---|
onEscapeKeydown | |
escapeKeydownBehavior | |
onInteractOutside | |
onFocusOutside | |
interactOutsideBehavior | |
onOpenAutoFocus | |
onCloseAutoFocus | |
trapFocus | |
preventScroll | |
preventOverflowTextSelection | |
dir | |
forceMount | |
loop | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-state | |
data-context-menu-content |
A menu item within the context menu.
| Property | Details |
|---|---|
disabled | |
textValue | |
onSelect | |
closeOnSelect | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-orientation | |
data-highlighted | |
data-disabled | |
data-context-menu-item |
A group of checkbox menu items, where multiple can be checked at a time.
| Property | Details |
|---|---|
value | |
onValueChange | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-context-menu-checkbox-group |
A menu item that can be controlled and toggled like a checkbox.
| Property | Details |
|---|---|
disabled | |
checked | |
onCheckedChange | |
indeterminate | |
onIndeterminateChange | |
value | |
textValue | |
onSelect | |
closeOnSelect | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-orientation | |
data-highlighted | |
data-disabled | |
data-state | |
data-context-menu-checkbox-item |
A group of radio menu items, where only one can be checked at a time.
| Property | Details |
|---|---|
value | |
onValueChange | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-context-menu-radio-group |
A menu item that can be controlled and toggled like a radio button. It must be a child of a RadioGroup.
| Property | Details |
|---|---|
value | |
disabled | |
textValue | |
onSelect | |
closeOnSelect | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-orientation | |
data-highlighted | |
data-disabled | |
data-state | |
data-value | |
data-context-menu-radio-item |
A horizontal line to visually separate menu items.
| Property | Details |
|---|---|
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-orientation | |
data-menu-separator | |
data-context-menu-separator |
An optional arrow which points to the context menu's anchor/trigger point.
| Property | Details |
|---|---|
width | |
height | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-state | |
data-context-menu-arrow |
A group of menu items. It should be passed an aria-label or have a child ContextMenu.GroupHeading component to provide a label for a group of menu items.
| Property | Details |
|---|---|
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-context-menu-group |
A heading for a group which will be skipped when navigating with the keyboard. It is used to provide a visual label for a group of menu items and must be a child of either a ContextMenu.Group or ContextMenu.RadioGroup component.
| Property | Details |
|---|---|
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-menu-group-heading |
A submenu belonging to the parent context menu. Responsible for managing the state of the submenu.
| Property | Details |
|---|---|
open | |
onOpenChange | |
onOpenChangeComplete | |
children |
A menu item which when pressed or hovered, opens the submenu it is a child of.
| Property | Details |
|---|---|
disabled | |
openDelay | |
textValue | |
onSelect | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-orientation | |
data-highlighted | |
data-disabled | |
data-state | |
data-context-menu-sub-trigger |
The submenu content displayed when the parent submenu is open.
| Property | Details |
|---|---|
side | |
sideOffset | |
align | |
alignOffset | |
arrowPadding | |
avoidCollisions | |
collisionBoundary | |
collisionPadding | |
sticky | |
hideWhenDetached | |
updatePositionStrategy | |
strategy | |
preventScroll | |
customAnchor | |
onEscapeKeydown | |
escapeKeydownBehavior | |
onInteractOutside | |
onFocusOutside | |
interactOutsideBehavior | |
onOpenAutoFocus | |
onCloseAutoFocus | |
trapFocus | |
forceMount | |
preventOverflowTextSelection | |
dir | |
loop | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-state | |
data-context-menu-sub-content |
The submenu content displayed when the parent submenu menu is open. (Static/No Floating UI)
| Property | Details |
|---|---|
onEscapeKeydown | |
escapeKeydownBehavior | |
onInteractOutside | |
onFocusOutside | |
interactOutsideBehavior | |
onOpenAutoFocus | |
onCloseAutoFocus | |
trapFocus | |
forceMount | |
preventOverflowTextSelection | |
dir | |
loop | |
ref | |
children | |
child |
| Data Attribute | Details |
|---|---|
data-state | |
data-context-menu-sub-content |