suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Wheel Picker
  • Widget

suiss/ui · Component

Context Menu

A menu of actions that opens at the pointer when people right-click an area.

Context Menu is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Context menu wraps the context menu primitive of the chosen base. The ContextMenu root manages the open state and ContextMenuTrigger defines the area that opens the menu. ContextMenuContent renders the menu in a portal at the pointer position. Inside it, use ContextMenuItem for actions, ContextMenuCheckboxItem for toggles, and ContextMenuRadioGroup with ContextMenuRadioItem for a single choice. Organize items with ContextMenuGroup, ContextMenuLabel and ContextMenuSeparator, and nest a submenu with ContextMenuSub, ContextMenuSubTrigger and ContextMenuSubContent. ContextMenuShortcut displays a keyboard shortcut at the end of an item.

Use a context menu for secondary actions on an object. Because it is hidden until a right-click or long press, make the same actions available elsewhere. For a menu opened from a visible button, use a dropdown menu.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/context-menu

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add context-menu

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @base-ui/react @phosphor-icons/react
  1. Add the pieces it builds on: use-platform.

  2. Copy the source into components/ui/context-menu.tsx:

  1. Update the import paths to match your project.

Usage

import { ContextMenu, ContextMenuPortal, ContextMenuTrigger, ContextMenuContent, ContextMenuGroup, ContextMenuLabel, ContextMenuItem, ContextMenuSub, ContextMenuSubTrigger, ContextMenuSubContent, ContextMenuCheckboxItem, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut } 
<ContextMenu


















































Checkbox items are uncontrolled with defaultChecked, or controlled with checked and onCheckedChange. Radio groups take defaultValue, or value and onValueChange, and each radio item needs a value. Respond to an action with the item's onClick. Selecting a regular item closes the menu; checkbox and radio items keep it open.

Checkbox items are uncontrolled with defaultSelected, or controlled with isSelected and onChange. Radio groups take defaultValue, or value and onChange, and each radio item needs a value. Respond to an action with the item's onAction. Selecting a regular item closes the menu; checkbox and radio items keep it open.

Set inset on items and labels that have no icon so their text aligns with items that do, or with the indicators of checkbox and radio items. Set variant="destructive" on items that delete or remove something.

Examples

Inset

A file menu where inset aligns a label and items without icons, ending with a destructive item.

API Reference

Anatomy18 parts
<ContextMenu>
<ContextMenuTrigger />
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuItem>
<ContextMenuShortcut />
</ContextMenuItem
01

ContextMenu

The root that manages the menu's open state.

Also accepts the props of the primitive part it wraps.

02

ContextMenuPortal

Renders its children into a portal outside the current DOM hierarchy.

Also accepts the props of the primitive part it wraps.

03

ContextMenuTrigger

The area that opens the menu on right-click.

Also accepts the props of the primitive part it wraps.

04

ContextMenuContent

The menu panel, rendered in a portal at the pointer position.

Also accepts the props of the primitive parts it wraps.

05

ContextMenuGroup

Groups related items.

Also accepts the props of the primitive part it wraps.

06

ContextMenuLabel

A heading for a group of items.

PropTypeDefaultDescription
insetboolean-Adds leading padding so the label aligns with items that have an icon or indicator.

Also accepts the props of the primitive part it wraps.

07

ContextMenuItem

An action in the menu.

PropTypeDefaultDescription
insetboolean-Adds leading padding so the item aligns with items that have an icon or indicator.
variant"default" | "destructive""default"destructive colors the item and its icon with the destructive color.

Also accepts the props of the primitive part it wraps.

08

ContextMenuSub

The root of a nested submenu.

Also accepts the props of the primitive part it wraps.

09

ContextMenuSubTrigger

The item that opens a submenu, shown with a trailing caret.

PropTypeDefaultDescription
insetboolean-Adds leading padding so the trigger aligns with items that have an icon or indicator.

Also accepts the props of the primitive part it wraps.

10

ContextMenuSubContent

The panel of a submenu, opened beside its trigger.

Also accepts the props of React.ComponentProps<typeof ContextMenuContent>.

11

ContextMenuCheckboxItem

An item that toggles a setting on or off and shows a check mark when on.

PropTypeDefaultDescription
insetboolean-Adds leading padding so the item aligns with other inset items.

Also accepts the props of the primitive part it wraps.

12

ContextMenuRadioGroup

Groups radio items so that only one can be selected.

Also accepts the props of the primitive part it wraps.

13

ContextMenuRadioItem

An item in a radio group that shows a check mark when selected.

PropTypeDefaultDescription
insetboolean-Adds leading padding so the item aligns with other inset items.

Also accepts the props of the primitive part it wraps.

14

ContextMenuSeparator

A divider between groups of items.

Also accepts the props of the primitive part it wraps.

15

ContextMenuShortcut

Displays a keyboard shortcut at the end of an item.

PropTypeDefaultDescription
modboolean-Prefixes the shortcut with the platform modifier key, ⌘ on Apple platforms and Ctrl+ elsewhere.

Also accepts the props of React.ComponentProps<"span">.

Accessibility

  • The menu has the menu role. Items have the menuitem, menuitemcheckbox or menuitemradio role, and checkbox and radio items set aria-checked.
  • Disabled items set aria-disabled and cannot be chosen.
  • Arrow keys move between items. On a submenu trigger, the Right Arrow opens the submenu and moves focus into it, and the Left Arrow returns to the trigger. The arrow directions follow the reading direction.
  • Escape closes the menu, and choosing a regular item closes it.
  • ContextMenuShortcut only displays a shortcut; it does not register one. It is hidden on small screens.

Notes

  • Styling hooks: data-slot values such as context-menu-content, context-menu-item, context-menu-checkbox-item, context-menu-radio-item, context-menu-sub-trigger, context-menu-label, context-menu-separator and context-menu-shortcut.
  • Items set data-inset and data-variant; disabled items get data-disabled, and an open submenu trigger gets data-popup-open.
  • By default the menu opens on the inline-end side of the pointer, aligned to start.
  • The trigger sets select-none so long-pressing does not select its text.
  • Uses the popover, accent, destructive and border colors.

ContextMenu attaches a menu to a ContextMenuTrigger area. A long press opens ContextMenuContent at the position of the finger with a medium impact haptic. The content renders through a portal (inside a full-window overlay on iOS) as a raised, rounded card.

Items can be plain actions, checkbox items, radio items in a group, or a submenu. ContextMenuSubContent expands in place under its trigger, and the caret turns to point down while it is open.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/context-menu

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add context-menu
$ pnpm dlx @esuiss/ui@latest add context-menu --platform swift
NextDialogPreviousCommand

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Inset
  4. 04API Reference
    • ContextMenu
    • ContextMenuPortal
    • ContextMenuTrigger
    • ContextMenuContent
    • ContextMenuGroup
    • ContextMenuLabel
    • ContextMenuItem
    • ContextMenuSub
    • ContextMenuSubTrigger
    • ContextMenuSubContent
    • ContextMenuCheckboxItem
    • ContextMenuRadioGroup
    • ContextMenuRadioItem
    • ContextMenuSeparator
    • ContextMenuShortcut
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • Default
    • Simple
  10. 10API Reference
    • ContextMenu
    • ContextMenuContent
    • ContextMenuSubTrigger
    • ContextMenuSubContent
    • ContextMenuItem
    • ContextMenuCheckboxItem
    • ContextMenuRadioItem
    • ContextMenuLabel
    • ContextMenuSeparator
  11. 11Accessibility
  12. 12Notes
from
"@/components/ui/context-menu"
>
<ContextMenuTrigger className="text-muted-foreground flex h-40 w-72 items-center justify-center rounded-2xl border border-dashed text-sm">
Right click here
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuGroup>
<ContextMenuItem>
<ArrowLeftIcon />
Back
<ContextMenuShortcut mod>[</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem disabled>
<ArrowRightIcon />
Forward
<ContextMenuShortcut mod>]</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
<ArrowClockwiseIcon />
Reload
<ContextMenuShortcut mod>R</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>More tools</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem>Save page as...</ContextMenuItem>
<ContextMenuItem>Create shortcut...</ContextMenuItem>
<ContextMenuItem>Developer tools</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuCheckboxItem defaultChecked>
Show bookmarks bar
</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem>Show full URLs</ContextMenuCheckboxItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuRadioGroup defaultValue="ada">
<ContextMenuLabel inset>People</ContextMenuLabel>
<ContextMenuRadioItem value="ada">Ada Lovelace</ContextMenuRadioItem>
<ContextMenuRadioItem value="alan">Alan Turing</ContextMenuRadioItem>
</ContextMenuRadioGroup>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive">
<TrashIcon />
Delete
<ContextMenuShortcut mod>⌫</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
>
<ContextMenuCheckboxItem />
<ContextMenuLabel />
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger />
<ContextMenuSubContent>
<ContextMenuItem />
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuRadioGroup>
<ContextMenuLabel />
<ContextMenuRadioItem />
</ContextMenuRadioGroup>
<ContextMenuItem>
<ContextMenuShortcut />
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
$ pnpm dlx @esuiss/ui@latest add context-menu --platform kotlin

The CLI also adds what it builds on: Icon, Text. On React Native it also adds the animated view components/ui/animated-view.tsx.

Manual

  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-reanimated react-native-screens react-native-svg react-native-worklets
  1. Add the pieces it builds on: Icon, Text, and the primitives components/primitives/context-menu.tsx, components/primitives/use-render.tsx, and the animated view components/ui/animated-view.tsx from the next step.

  2. Copy the animated view into components/ui/animated-view.tsx:

uniwind/components/ui/animated-view.tsx
import { Pressable } from "react-native"
import Animated from "react-native-reanimated"

const AnimatedPressable = Animated.createAnimatedComponent(Pressable)

type AnimatedViewProps =
  | (React.ComponentProps<typeof Animated.View> &
      React.RefAttributes<React.ComponentRef<typeof Animated.View>> & {
        as?: "View"
      })
  | (React.ComponentProps<typeof AnimatedPressable> &
      React.RefAttributes<React.ComponentRef<typeof AnimatedPressable>> & {
        as: "Pressable"
      })

function AnimatedView(props: AnimatedViewProps) {
  1. Copy the source into components/ui/context-menu.tsx:
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-reanimated react-native-screens react-native-svg react-native-worklets
  1. Add the pieces it builds on: Icon, Text, and the primitives components/primitives/context-menu.tsx, components/primitives/use-render.tsx, and the animated view components/ui/animated-view.tsx from the next step.

  2. Copy the animated view into components/ui/animated-view.tsx:

  1. Copy the source into components/ui/context-menu.tsx:
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install phosphor-react-native react-native-reanimated react-native-screens react-native-svg react-native-worklets
  1. Add the pieces it builds on: Icon, Text, and the primitives components/primitives/context-menu.tsx, components/primitives/use-render.tsx, and the animated view components/ui/animated-view.tsx from the next step.

  2. Copy the animated view into components/ui/animated-view.tsx:

  1. Copy the source into components/ui/context-menu.tsx:
  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Add the Swift package to your app:

Package.swift
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
  1. Copy the theme files every view uses:
swift/Theme/Theme.swift
import SwiftUI
import UIKit

struct RGBA: Sendable {
  let red: CGFloat
  let green: CGFloat
  let blue: CGFloat
  let alpha: CGFloat

  init(_ hex: UInt32, _ alpha: CGFloat = 1) {
    red = CGFloat((hex >> 16) & 0xFF) / 255
    green = CGFloat((hex >> 8) & 0xFF) / 255
    blue = CGFloat(hex & 0xFF) / 255
    self.alpha = alpha
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
  1. Copy the source and every file it uses:
swift/Components/Button.swift
import SwiftUI

public struct VariantButtonStyle: ButtonStyle {
  public enum Variant: CaseIterable, Sendable {
    case `default`, outline, secondary, tinted, ghost, plain, destructive, link
  }

  public enum Size: CaseIterable, Sendable {
    case `default`, xs, sm, lg, xl, icon, iconXs, iconSm, iconLg, iconXl

    var height: CGFloat {
      switch self {
      case .xs, .iconXs: 24
      case .sm, .iconSm: 32
      case .default, .icon: 36
      case .lg, .iconLg: 44
swift/Components/ButtonGroup.swift
import SwiftUI

extension EnvironmentValues {
  @Entry var buttonGroup: Axis? = nil
  @Entry var buttonGroupSize: CGSize = .zero
}

enum ButtonGroupSpace: Hashable {
  case group
}

struct ButtonGroupFrame {
  let bounds: Anchor<CGRect>
  let outlined: Bool
}
swift/Components/DropdownMenu.swift
import PhosphorSwift
import SwiftUI

enum MenuPresentation: Sendable {
  case system, popup
}

extension EnvironmentValues {
  @Entry var menuPresentation: MenuPresentation = .system
  @Entry var menuRadio = MenuRadioContext()
}

struct MenuRadioContext: @unchecked Sendable {
  var selected: AnyHashable?
  var select: @MainActor @Sendable (AnyHashable) -> Void = { _ in }
}
swift/Components/Field.swift
import SwiftUI

extension EnvironmentValues {
  @Entry public var isInvalid = false
  @Entry var fieldInset = false
  @Entry var fieldGroupInset = false
  @Entry var fieldSetInset = false
}

extension View {
  public func invalid(_ isInvalid: Bool = true) -> some View {
    environment(\.isInvalid, isInvalid)
  }
}

public enum FieldOrientation: CaseIterable, Sendable {
swift/Components/Overlay.swift
import PhosphorSwift
import Observation
import SwiftUI

public enum PopupAlignment: Sendable, CaseIterable {
  case start, center, end
}

struct OverlayDismiss: Sendable {
  let action: @MainActor @Sendable () -> Void

  @MainActor func callAsFunction() {
    action()
  }
}
swift/Components/Select.swift
import PhosphorSwift
import SwiftUI

struct SelectContext: @unchecked Sendable {
  var selected: AnyHashable?
  var registering = false
  var select: @MainActor @Sendable (AnyHashable) -> Void = { _ in }
}

extension EnvironmentValues {
  @Entry var select = SelectContext()
}

struct SelectLabelsKey: PreferenceKey {
  static var defaultValue: [AnyHashable: Text] { [:] }
swift/Components/Separator.swift
import SwiftUI

public struct Separator: View {
  let orientation: Axis

  public init(_ orientation: Axis = .horizontal) {
    self.orientation = orientation
  }

  public var body: some View {
    Rectangle()
      .fill(EsuissColor.border)
      .frame(width: orientation == .vertical ? 1 : nil, height: orientation == .horizontal ? 1 : nil)
      .frame(maxWidth: orientation == .horizontal ? .infinity : nil, maxHeight: orientation == .vertical ? .infinity : nil)
      .accessibilityHidden(true)
  }
swift/Components/TextVariant.swift
import SwiftUI

public enum TextVariant: CaseIterable, Sendable {
  case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted

  var style: TextStyle {
    switch self {
    case .h1: .xl3
    case .h2: .xl2
    case .h3: .xl
    case .h4, .lead, .large: .lg
    case .code, .small, .muted: .sm
    default: .base
    }
  }
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
build.gradle.kts
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
  1. Copy the theme files every view uses:
kotlin/theme/Theme.kt
package com.esuiss.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Immutable
data class Colors(
    val background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
    val primaryForeground: Color,
  1. Copy the source and every file it uses:
kotlin/components/Button.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
kotlin/components/ButtonGroup.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
kotlin/components/Checkbox.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.triStateToggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
kotlin/components/Content.kt
package com.esuiss.ui.components

import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val LocalContentStyle = compositionLocalOf { TextStyle.Default }

val LocalContentColor = compositionLocalOf { Color.Unspecified }

val LocalIconSize = compositionLocalOf { 20.dp }
kotlin/components/Field.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.LayoutScopeMarker
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
kotlin/components/FormSupport.kt
package com.esuiss.ui.components

import android.provider.Settings
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
kotlin/components/Haptics.kt
package com.esuiss.ui.components

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

val LocalHapticsEnabled = staticCompositionLocalOf { true }

@Composable
fun ProvideHaptics(enabled: Boolean, content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalHapticsEnabled provides enabled, content = content)
}
kotlin/components/Icon.kt
package com.esuiss.ui.components

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Icon(
    imageVector: ImageVector,
kotlin/components/Label.kt
package com.esuiss.ui.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
kotlin/components/Separator.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Separator(
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { ContextMenuContent, ContextMenuSubTrigger, ContextMenuSubContent, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioItem, ContextMenuLabel, ContextMenuSeparator } from "@/components/ui/context-menu"
<ContextMenuDemo defaultOpen />

Wrap the long-pressable area in ContextMenuTrigger and list ContextMenuItems in ContextMenuContent. Items close the menu when pressed unless closeOnPress={false}.

Use ContextMenuCheckboxItem with checked and onCheckedChange, and ContextMenuRadioItem inside a ContextMenuRadioGroup with value and onValueChange. Group items with ContextMenuGroup and ContextMenuLabel, and divide sections with ContextMenuSeparator.

Set variant="destructive" on an item that deletes or removes. Use inset on items without an icon to align them with items that have one.

Examples

Default

iOS
Context Menu on iOS in light modeContext Menu on iOS in dark mode
import * as React from "react"
import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger } from "@/components/ui/context-menu"
import { Text } from "@/components/ui/text"

function TriggerArea() {
  return (
    <ContextMenuTrigger className="border-border h-36 w-full items-center justify-center rounded-md border border-dashed">
      <Text className="text-sm text-muted-foreground">Long press here</Text>
    </ContextMenuTrigger>
  )
}

function ContextMenuDemo({ defaultOpen }: { defaultOpen?: boolean }) {
  const [showBookmarks, setShowBookmarks] = React.useState(true)
  const [showUrls, setShowUrls] = React.useState(false)
  const [person, setPerson] = React.useState("pedro")
  return (
    <ContextMenu defaultOpen={defaultOpen}>
      <TriggerArea />
      <ContextMenuContent className="w-64">
        <ContextMenuItem inset>
          <Text>Back</Text>
        </ContextMenuItem>
        <ContextMenuItem inset disabled>
          <Text>Forward</Text>
        </ContextMenuItem>
        <ContextMenuItem inset>
          <Text>Reload</Text>
        </ContextMenuItem>
        <ContextMenuSub>
          <ContextMenuSubTrigger inset>
            <Text>More tools</Text>
          </ContextMenuSubTrigger>
          <ContextMenuSubContent className="w-48">
            <ContextMenuItem>
              <Text>Save page</Text>
            </ContextMenuItem>
            <ContextMenuItem>
              <Text>Create shortcut</Text>
            </ContextMenuItem>
            <ContextMenuSeparator />
            <ContextMenuItem variant="destructive">
              <Text>Delete</Text>
            </ContextMenuItem>
          </ContextMenuSubContent>
        </ContextMenuSub>
        <ContextMenuSeparator />
        <ContextMenuCheckboxItem
          checked={showBookmarks}
          onCheckedChange={setShowBookmarks}
        >
          <Text>Show bookmarks</Text>
        </ContextMenuCheckboxItem>
        <ContextMenuCheckboxItem
          checked={showUrls}
          onCheckedChange={setShowUrls}
        >
          <Text>Show full URLs</Text>
        </ContextMenuCheckboxItem>
        <ContextMenuSeparator />
        <ContextMenuRadioGroup value={person} onValueChange={setPerson}>
          <ContextMenuLabel inset>People</ContextMenuLabel>
          <ContextMenuRadioItem value="pedro">
            <Text>Pedro Duarte</Text>
          </ContextMenuRadioItem>
          <ContextMenuRadioItem value="colm">
            <Text>Colm Tuite</Text>
          </ContextMenuRadioItem>
        </ContextMenuRadioGroup>
      </ContextMenuContent>
    </ContextMenu>
  )
}

export function ContextMenuExample() {
  return (
    <ContextMenuDemo />
  )
}
import EsuissUI
import SwiftUI

struct ContextMenuTriggerArea: View {
  var body: some View {
    Text("Long press here")
      .textStyle(.sm)
      .foregroundStyle(EsuissColor.mutedForeground)
      .frame(maxWidth: .infinity)
      .frame(height: 144)
      .background(EsuissColor.background, in: RoundedRectangle(cornerRadius: Radius.md, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.md, style: .continuous)
          .strokeBorder(EsuissColor.border, style: StrokeStyle(lineWidth: 1, dash: [4, 3]))
      }
  }
}

struct ContextMenuDemo: View {
  @State private var showBookmarks = true
  @State private var showUrls = false
  @State private var person = "pedro"

  var body: some View {
    ContextMenuTriggerArea()
      .contextMenu(previewRadius: Radius.md) {
        ContextMenuItem("Back", inset: true) {}
        ContextMenuItem("Forward", inset: true) {}
          .disabled(true)
        ContextMenuItem("Reload", inset: true) {}
        ContextMenuSub("More tools", inset: true) {
          ContextMenuItem("Save page") {}
          ContextMenuItem("Create shortcut") {}
          ContextMenuSeparator()
          ContextMenuItem("Delete", variant: .destructive) {}
        }
        ContextMenuSeparator()
        ContextMenuCheckboxItem("Show bookmarks", isOn: $showBookmarks)
        ContextMenuCheckboxItem("Show full URLs", isOn: $showUrls)
        ContextMenuSeparator()
        ContextMenuRadioGroup(selection: $person) {
          ContextMenuLabel("People", inset: true)
          ContextMenuRadioItem("Pedro Duarte", value: "pedro")
          ContextMenuRadioItem("Colm Tuite", value: "colm")
        }
      }
  }
}
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ContextMenu
import com.esuiss.ui.components.DropdownMenuCheckboxItem
import com.esuiss.ui.components.DropdownMenuItem
import com.esuiss.ui.components.DropdownMenuLabel
import com.esuiss.ui.components.DropdownMenuRadioGroup
import com.esuiss.ui.components.DropdownMenuRadioItem
import com.esuiss.ui.components.DropdownMenuSeparator
import com.esuiss.ui.components.DropdownMenuSub
import com.esuiss.ui.components.MenuItemVariant
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography

@Composable
fun ContextMenuExample() {
    ContextMenuDemo(defaultOpen = false)
}

@Composable
fun ContextMenuTriggerArea() {
    val color = EsuissTheme.colors.border
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(144.dp)
            .drawBehind {
                val stroke = 1.dp.toPx()
                drawRoundRect(
                    color = color,
                    topLeft = Offset(stroke / 2, stroke / 2),
                    size = Size(size.width - stroke, size.height - stroke),
                    cornerRadius = CornerRadius(Radius.md.toPx()),
                    style = Stroke(stroke, pathEffect = PathEffect.dashPathEffect(floatArrayOf(4.dp.toPx(), 3.dp.toPx()))),
                )
            },
        contentAlignment = Alignment.Center,
    ) {
        Text("Long press here", style = Typography.sm.copy(color = EsuissTheme.colors.mutedForeground))
    }
}

@Composable
fun ContextMenuDemo(defaultOpen: Boolean) {
    var open by rememberSaveable { mutableStateOf(defaultOpen) }
    var showBookmarks by rememberSaveable { mutableStateOf(true) }
    var showUrls by rememberSaveable { mutableStateOf(false) }
    var person by rememberSaveable { mutableStateOf("pedro") }
    ContextMenu(
        expanded = open,
        onExpandedChange = { open = it },
        trigger = { ContextMenuTriggerArea() },
        modifier = Modifier.width(256.dp),
    ) {
        DropdownMenuItem("Back", onClick = {}, inset = true)
        DropdownMenuItem("Forward", onClick = {}, inset = true, enabled = false)
        DropdownMenuItem("Reload", onClick = {}, inset = true)
        DropdownMenuSub("More tools", inset = true) {
            Column(Modifier.width(192.dp)) {
                DropdownMenuItem("Save page", onClick = {})
                DropdownMenuItem("Create shortcut", onClick = {})
                DropdownMenuSeparator()
                DropdownMenuItem("Delete", onClick = {}, variant = MenuItemVariant.Destructive)
            }
        }
        DropdownMenuSeparator()
        DropdownMenuCheckboxItem("Show bookmarks", checked = showBookmarks, onCheckedChange = { showBookmarks = it })
        DropdownMenuCheckboxItem("Show full URLs", checked = showUrls, onCheckedChange = { showUrls = it })
        DropdownMenuSeparator()
        DropdownMenuRadioGroup(value = person, onValueChange = { person = it }) {
            DropdownMenuLabel("People", inset = true)
            DropdownMenuRadioItem("pedro", "Pedro Duarte")
            DropdownMenuRadioItem("colm", "Colm Tuite")
        }
    }
}

Simple

A short list of plain items.

iOS
Context Menu (simple) on iOS in light modeContext Menu (simple) on iOS in dark mode
import { View } from "react-native"
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "@/components/ui/context-menu"
import { Text } from "@/components/ui/text"

function TriggerArea() {
  return (
    <ContextMenuTrigger className="border-border h-36 w-full items-center justify-center rounded-md border border-dashed">
      <Text className="text-sm text-muted-foreground">Long press here</Text>
    </ContextMenuTrigger>
  )
}

export function ContextMenuSimpleExample() {
  return (
    <View>
      <ContextMenu>
        <TriggerArea />
        <ContextMenuContent className="w-48">
          <ContextMenuItem>
            <Text>Profile</Text>
          </ContextMenuItem>
          <ContextMenuItem>
            <Text>Billing</Text>
          </ContextMenuItem>
          <ContextMenuItem>
            <Text>Team</Text>
          </ContextMenuItem>
          <ContextMenuItem>
            <Text>Subscription</Text>
          </ContextMenuItem>
        </ContextMenuContent>
      </ContextMenu>
    </View>
  )
}
import EsuissUI
import SwiftUI

struct ContextMenuSimpleExample: View {
  var body: some View {
    ContextMenuTriggerArea()
      .contextMenu(previewRadius: Radius.md) {
        ContextMenuItem("Profile") {}
        ContextMenuItem("Billing") {}
        ContextMenuItem("Team") {}
        ContextMenuItem("Subscription") {}
      }
  }
}

struct ContextMenuTriggerArea: View {
  var body: some View {
    Text("Long press here")
      .textStyle(.sm)
      .foregroundStyle(EsuissColor.mutedForeground)
      .frame(maxWidth: .infinity)
      .frame(height: 144)
      .background(EsuissColor.background, in: RoundedRectangle(cornerRadius: Radius.md, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.md, style: .continuous)
          .strokeBorder(EsuissColor.border, style: StrokeStyle(lineWidth: 1, dash: [4, 3]))
      }
  }
}
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ContextMenu
import com.esuiss.ui.components.DropdownMenuItem
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography

@Composable
fun ContextMenuTriggerArea() {
    val color = EsuissTheme.colors.border
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(144.dp)
            .drawBehind {
                val stroke = 1.dp.toPx()
                drawRoundRect(
                    color = color,
                    topLeft = Offset(stroke / 2, stroke / 2),
                    size = Size(size.width - stroke, size.height - stroke),
                    cornerRadius = CornerRadius(Radius.md.toPx()),
                    style = Stroke(stroke, pathEffect = PathEffect.dashPathEffect(floatArrayOf(4.dp.toPx(), 3.dp.toPx()))),
                )
            },
        contentAlignment = Alignment.Center,
    ) {
        Text("Long press here", style = Typography.sm.copy(color = EsuissTheme.colors.mutedForeground))
    }
}

@Composable
fun ContextMenuSimple() {
    var open by rememberSaveable { mutableStateOf(false) }
    ContextMenu(
        expanded = open,
        onExpandedChange = { open = it },
        trigger = { ContextMenuTriggerArea() },
        modifier = Modifier.width(192.dp),
    ) {
        DropdownMenuItem("Profile", onClick = {})
        DropdownMenuItem("Billing", onClick = {})
        DropdownMenuItem("Team", onClick = {})
        DropdownMenuItem("Subscription", onClick = {})
    }
}

API Reference

01

ContextMenu

The root that manages the open state.

PropTypeDefaultDescription
openboolean-Whether the menu is open, for controlled use.
defaultOpenbooleanfalseWhether the menu starts open when uncontrolled.
onOpenChange(open: boolean, details) => void-Called when the menu opens or closes.
disabledbooleanfalsePrevents the menu from opening.
02

ContextMenuContent

Floating menu card.

PropTypeDefaultDescription
portalHoststring-Name of the portal host to render into.

Also accepts the props of React.ComponentProps<typeof ContextMenuPrimitive.Popup>, PositionerProps.

03

ContextMenuSubTrigger

Row that expands the submenu.

PropTypeDefaultDescription
childrenReact.ReactNode-
iconClassNamestring-Extra classes for the caret icon.
insetboolean-Adds start padding.

Also accepts the props of React.ComponentProps<typeof ContextMenuPrimitive.SubmenuTrigger>.

04

ContextMenuSubContent

Items of the submenu.

Also accepts the props of React.ComponentProps<typeof ContextMenuPrimitive.Popup>.

05

ContextMenuItem

Action row.

PropTypeDefaultDescription
insetboolean-Adds start padding to align with icon items.
variant"default" | "destructive""default"default or destructive.

Also accepts the props of React.ComponentProps<typeof ContextMenuPrimitive.Item>.

06

ContextMenuCheckboxItem

Row with a check mark that toggles.

PropTypeDefaultDescription
childrenReact.ReactNode-
insetboolean-Adds start padding.

Also accepts the props of React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>.

07

ContextMenuRadioItem

Row that selects one value in its group.

PropTypeDefaultDescription
childrenReact.ReactNode-
insetboolean-Adds start padding.

Also accepts the props of React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>.

08

ContextMenuLabel

Heading for a group.

PropTypeDefaultDescription
insetboolean-Adds start padding.

Also accepts the props of React.ComponentProps<typeof ContextMenuPrimitive.GroupLabel>.

09

ContextMenuSeparator

Line between sections.

Also accepts the props of React.ComponentProps<typeof ContextMenuPrimitive.Separator>.

Accessibility

  • The trigger exposes a longpress accessibility action, so VoiceOver and TalkBack users can open the menu, and reports its expanded state.
  • The popup has the menu role; items use menuitem, checkbox and radio roles, and radio groups use radiogroup.
  • Separators have the separator role.

Notes

  • Uses the bg-popover token; a highlighted item fades to opacity-80 and a disabled one to opacity-50; light and dark follow the system appearance.
  • The fade-in respects the system Reduce Motion setting.
iOS
Context Menu (open by default) on iOS in light modeContext Menu (open by default) on iOS in dark mode
import * as React from "react"
import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger } from "@/components/ui/context-menu"
import { Text } from "@/components/ui/text"

function TriggerArea() {
  return (
    <ContextMenuTrigger className="border-border h-36 w-full items-center justify-center rounded-md border border-dashed">
      <Text className="text-sm text-muted-foreground">Long press here</Text>
    </ContextMenuTrigger>
  )
}

function ContextMenuDemo({ defaultOpen }: { defaultOpen?: boolean }) {
  const [showBookmarks, setShowBookmarks] = React.useState(true)
  const [showUrls, setShowUrls] = React.useState(false)
  const [person, setPerson] = React.useState("pedro")
  return (
    <ContextMenu defaultOpen={defaultOpen}>
      <TriggerArea />
      <ContextMenuContent className="w-64">
        <ContextMenuItem inset>
          <Text>Back</Text>
        </ContextMenuItem>
        <ContextMenuItem inset disabled>
          <Text>Forward</Text>
        </ContextMenuItem>
        <ContextMenuItem inset>
          <Text>Reload</Text>
        </ContextMenuItem>
        <ContextMenuSub>
          <ContextMenuSubTrigger inset>
            <Text>More tools</Text>
          </ContextMenuSubTrigger>
          <ContextMenuSubContent className="w-48">
            <ContextMenuItem>
              <Text>Save page</Text>
            </ContextMenuItem>
            <ContextMenuItem>
              <Text>Create shortcut</Text>
            </ContextMenuItem>
            <ContextMenuSeparator />
            <ContextMenuItem variant="destructive">
              <Text>Delete</Text>
            </ContextMenuItem>
          </ContextMenuSubContent>
        </ContextMenuSub>
        <ContextMenuSeparator />
        <ContextMenuCheckboxItem
          checked={showBookmarks}
          onCheckedChange={setShowBookmarks}
        >
          <Text>Show bookmarks</Text>
        </ContextMenuCheckboxItem>
        <ContextMenuCheckboxItem
          checked={showUrls}
          onCheckedChange={setShowUrls}
        >
          <Text>Show full URLs</Text>
        </ContextMenuCheckboxItem>
        <ContextMenuSeparator />
        <ContextMenuRadioGroup value={person} onValueChange={setPerson}>
          <ContextMenuLabel inset>People</ContextMenuLabel>
          <ContextMenuRadioItem value="pedro">
            <Text>Pedro Duarte</Text>
          </ContextMenuRadioItem>
          <ContextMenuRadioItem value="colm">
            <Text>Colm Tuite</Text>
          </ContextMenuRadioItem>
        </ContextMenuRadioGroup>
      </ContextMenuContent>
    </ContextMenu>
  )
}

export function ContextMenuOpenByDefaultExample() {
  return (
    <ContextMenuDemo defaultOpen />
  )
}
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ContextMenu
import com.esuiss.ui.components.DropdownMenuCheckboxItem
import com.esuiss.ui.components.DropdownMenuItem
import com.esuiss.ui.components.DropdownMenuLabel
import com.esuiss.ui.components.DropdownMenuRadioGroup
import com.esuiss.ui.components.DropdownMenuRadioItem
import com.esuiss.ui.components.DropdownMenuSeparator
import com.esuiss.ui.components.DropdownMenuSub
import com.esuiss.ui.components.MenuItemVariant
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography

@Composable
fun ContextMenuOpenByDefaultExample() {
    ContextMenuDemo(defaultOpen = true)
}

@Composable
fun ContextMenuTriggerArea() {
    val color = EsuissTheme.colors.border
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(144.dp)
            .drawBehind {
                val stroke = 1.dp.toPx()
                drawRoundRect(
                    color = color,
                    topLeft = Offset(stroke / 2, stroke / 2),
                    size = Size(size.width - stroke, size.height - stroke),
                    cornerRadius = CornerRadius(Radius.md.toPx()),
                    style = Stroke(stroke, pathEffect = PathEffect.dashPathEffect(floatArrayOf(4.dp.toPx(), 3.dp.toPx()))),
                )
            },
        contentAlignment = Alignment.Center,
    ) {
        Text("Long press here", style = Typography.sm.copy(color = EsuissTheme.colors.mutedForeground))
    }
}

@Composable
fun ContextMenuDemo(defaultOpen: Boolean) {
    var open by rememberSaveable { mutableStateOf(defaultOpen) }
    var showBookmarks by rememberSaveable { mutableStateOf(true) }
    var showUrls by rememberSaveable { mutableStateOf(false) }
    var person by rememberSaveable { mutableStateOf("pedro") }
    ContextMenu(
        expanded = open,
        onExpandedChange = { open = it },
        trigger = { ContextMenuTriggerArea() },
        modifier = Modifier.width(256.dp),
    ) {
        DropdownMenuItem("Back", onClick = {}, inset = true)
        DropdownMenuItem("Forward", onClick = {}, inset = true, enabled = false)
        DropdownMenuItem("Reload", onClick = {}, inset = true)
        DropdownMenuSub("More tools", inset = true) {
            Column(Modifier.width(192.dp)) {
                DropdownMenuItem("Save page", onClick = {})
                DropdownMenuItem("Create shortcut", onClick = {})
                DropdownMenuSeparator()
                DropdownMenuItem("Delete", onClick = {}, variant = MenuItemVariant.Destructive)
            }
        }
        DropdownMenuSeparator()
        DropdownMenuCheckboxItem("Show bookmarks", checked = showBookmarks, onCheckedChange = { showBookmarks = it })
        DropdownMenuCheckboxItem("Show full URLs", checked = showUrls, onCheckedChange = { showUrls = it })
        DropdownMenuSeparator()
        DropdownMenuRadioGroup(value = person, onValueChange = { person = it }) {
            DropdownMenuLabel("People", inset = true)
            DropdownMenuRadioItem("pedro", "Pedro Duarte")
            DropdownMenuRadioItem("colm", "Colm Tuite")
        }
    }
}
nativewind/components/ui/animated-view.tsx
import { Pressable } from "react-native"
import Animated from "react-native-reanimated"

const AnimatedPressable = Animated.createAnimatedComponent(Pressable)

type AnimatedViewProps =
  | (React.ComponentProps<typeof Animated.View> &
      React.RefAttributes<React.ComponentRef<typeof Animated.View>> & {
        as?: "View"
      })
  | (React.ComponentProps<typeof AnimatedPressable> &
      React.RefAttributes<React.ComponentRef<typeof AnimatedPressable>> & {
        as: "Pressable"
      })

function AnimatedView(props: AnimatedViewProps) {
stylesheet/components/ui/animated-view.tsx
import { Pressable } from "react-native"
import Animated from "react-native-reanimated"

const AnimatedPressable = Animated.createAnimatedComponent(Pressable)

type AnimatedViewProps =
  | (React.ComponentProps<typeof Animated.View> &
      React.RefAttributes<React.ComponentRef<typeof Animated.View>> & {
        as?: "View"
      })
  | (React.ComponentProps<typeof AnimatedPressable> &
      React.RefAttributes<React.ComponentRef<typeof AnimatedPressable>> & {
        as: "Pressable"
      })

function AnimatedView(props: AnimatedViewProps) {
import {
  ArrowClockwiseIcon,
  ArrowLeftIcon,
  ArrowRightIcon,
  TrashIcon,
} from "@phosphor-icons/react"
import {
  ContextMenu,
  ContextMenuCheckboxItem,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuItem,
  ContextMenuLabel,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"

export default function ContextMenuDemo() {
  return (
    <ContextMenu>
      <ContextMenuTrigger className="text-muted-foreground flex h-40 w-72 items-center justify-center rounded-2xl border border-dashed text-sm">
        Right click here
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuGroup>
          <ContextMenuItem>
            <ArrowLeftIcon />
            Back
            <ContextMenuShortcut mod>[</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem disabled>
            <ArrowRightIcon />
            Forward
            <ContextMenuShortcut mod>]</ContextMenuShortcut>
          </ContextMenuItem>
          <ContextMenuItem>
            <ArrowClockwiseIcon />
            Reload
            <ContextMenuShortcut mod>R</ContextMenuShortcut>
          </ContextMenuItem>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuSub>
          <ContextMenuSubTrigger>More tools</ContextMenuSubTrigger>
          <ContextMenuSubContent>
            <ContextMenuItem>Save page as...</ContextMenuItem>
            <ContextMenuItem>Create shortcut...</ContextMenuItem>
            <ContextMenuItem>Developer tools</ContextMenuItem>
          </ContextMenuSubContent>
        </ContextMenuSub>
        <ContextMenuSeparator />
        <ContextMenuGroup>
          <ContextMenuCheckboxItem defaultChecked>
            Show bookmarks bar
          </ContextMenuCheckboxItem>
          <ContextMenuCheckboxItem>Show full URLs</ContextMenuCheckboxItem>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuRadioGroup defaultValue="ada">
          <ContextMenuLabel inset>People</ContextMenuLabel>
          <ContextMenuRadioItem value="ada">Ada Lovelace</ContextMenuRadioItem>
          <ContextMenuRadioItem value="alan">Alan Turing</ContextMenuRadioItem>
        </ContextMenuRadioGroup>
        <ContextMenuSeparator />
        <ContextMenuItem variant="destructive">
          <TrashIcon />
          Delete
          <ContextMenuShortcut mod>⌫</ContextMenuShortcut>
        </ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuItem,
  ContextMenuLabel,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"

export default function ContextMenuInset() {
  return (
    <ContextMenu>
      <ContextMenuTrigger className="text-muted-foreground flex h-40 w-72 items-center justify-center rounded-2xl border border-dashed text-sm">
        Right click a file
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuGroup>
          <ContextMenuLabel inset>report.pdf</ContextMenuLabel>
          <ContextMenuItem inset>Open</ContextMenuItem>
          <ContextMenuItem inset>Rename</ContextMenuItem>
          <ContextMenuItem inset>Duplicate</ContextMenuItem>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuItem inset variant="destructive">
          Move to trash
        </ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
components/ui/context-menu.tsx
"use client"

import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
import { CaretRightIcon, CheckIcon } from "@phosphor-icons/react"
import type * as React from "react"
import { useModifierKey } from "@/hooks/use-platform"
import { cn } from "@/lib/utils"

function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
  return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}

function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) {
  return (
    <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
  )
uniwind/components/ui/context-menu.tsx
import { cn } from "cn"
import { CaretRightIcon, CheckIcon } from "phosphor-react-native"
import * as React from "react"
import { Platform, View } from "react-native"
import { FadeIn, ReduceMotion } from "react-native-reanimated"
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"
import * as ContextMenuPrimitive from "@/components/primitives/context-menu"
import { resolveClassName } from "@/components/primitives/use-render"
import { AnimatedView } from "@/components/ui/animated-view"
import { Icon } from "@/components/ui/icon"
import { TextClassContext } from "@/components/ui/text"

const ContextMenu = ContextMenuPrimitive.Root

const ContextMenuTrigger = ContextMenuPrimitive.Trigger
swift/Components/ContextMenu.swift
import SwiftUI

public typealias ContextMenuItem<Label: View> = DropdownMenuItem<Label>
public typealias ContextMenuCheckboxItem<Label: View> = DropdownMenuCheckboxItem<Label>
public typealias ContextMenuRadioGroup<Content: View> = DropdownMenuRadioGroup<Content>
public typealias ContextMenuRadioItem<Label: View> = DropdownMenuRadioItem<Label>
public typealias ContextMenuLabel = DropdownMenuLabel
public typealias ContextMenuGroup<Content: View> = DropdownMenuGroup<Content>
public typealias ContextMenuSeparator = DropdownMenuSeparator
public typealias ContextMenuSub<Label: View, Content: View> = DropdownMenuSub<Label, Content>

extension View {
  public func contextMenu<Items: View, Preview: View>(
    @ViewBuilder items: () -> Items,
    @ViewBuilder preview: () -> Preview,
    previewRadius: CGFloat
kotlin/components/DropdownMenu.kt
package com.esuiss.ui.components

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
kotlin/components/ContextMenu.kt
package com.esuiss.ui.components

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.semantics.onLongClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
kotlin/components/Overlay.kt
package com.esuiss.ui.components

import android.os.Build
import android.view.Window
import android.view.WindowManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideIn
nativewind/components/ui/context-menu.tsx
import { cn } from "cn"
import { CaretRightIcon, CheckIcon } from "phosphor-react-native"
import * as React from "react"
import { Platform, View } from "react-native"
import { FadeIn, ReduceMotion } from "react-native-reanimated"
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"
import * as ContextMenuPrimitive from "@/components/primitives/context-menu"
import { resolveClassName } from "@/components/primitives/use-render"
import { AnimatedView } from "@/components/ui/animated-view"
import { Icon } from "@/components/ui/icon"
import { TextClassContext } from "@/components/ui/text"

const ContextMenu = ContextMenuPrimitive.Root

const ContextMenuTrigger = ContextMenuPrimitive.Trigger
stylesheet/components/ui/context-menu.tsx
import type { IconProps as PhosphorIconProps } from "phosphor-react-native"
import { CaretRightIcon, CheckIcon } from "phosphor-react-native"
import * as React from "react"
import {
  Platform,
  type StyleProp,
  StyleSheet,
  type TextStyle,
  View,
  type ViewStyle,
} from "react-native"
import { FadeIn, ReduceMotion } from "react-native-reanimated"
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"
import * as ContextMenuPrimitive from "@/components/primitives/context-menu"
import { resolveStyle } from "@/components/primitives/use-render"
import { AnimatedView } from "@/components/ui/animated-view"