覆蓋層與回饋
右鍵選單
在游標位置開啟的右鍵選單:子選單延遲展開、核取與單選項、快捷鍵標示,邊緣自動翻轉,觸控長按也能開啟。
在游標位置開啟的右鍵選單。在 Trigger 區塊按右鍵,選單就從游標點長出來;右側放不下會翻到游標左側、下方放不下會整塊往上推。提供一般項目、核取項、單選群組、群組標題、分隔線與快捷鍵標示,子選單在 hover 停留後從側邊滑入,並用「安全三角形」讓指標斜向移往子選單時不會誤關。鍵盤可完整操作(方向鍵、Enter、Escape、首字跳轉),觸控裝置長按 500ms 開啟。整組元件以 ContextMenu 搭配靜態屬性(ContextMenu.Item 等)組合,不依賴任何 headless 套件。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add context-menuPlayground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
100
150
編輯中的呼叫預設屬性
<ContextMenu/>
完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。
安裝
npx webberui@latest add https://webberui.com/r/context-menu.json或在 components.json 設定 registries 後,改用 @webberui/context-menu 安裝。
使用
import { ContextMenu } from "@/components/ui/context-menu";
const [starred, setStarred] = React.useState(false);
const [view, setView] = React.useState("grid");
<ContextMenu onOpenChange={(open) => console.log(open)}>
<ContextMenu.Trigger className="rounded-xl border p-4">
2026 第四季預算.xlsx
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item onSelect={() => console.log("開啟")}>
開啟
<ContextMenu.Shortcut>⌘O</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Sub>
<ContextMenu.SubTrigger>移動到</ContextMenu.SubTrigger>
<ContextMenu.SubContent>
<ContextMenu.Item>我的雲端硬碟</ContextMenu.Item>
<ContextMenu.Item>團隊共享</ContextMenu.Item>
</ContextMenu.SubContent>
</ContextMenu.Sub>
<ContextMenu.Separator />
<ContextMenu.CheckboxItem checked={starred} onCheckedChange={setStarred}>
加上星號
</ContextMenu.CheckboxItem>
<ContextMenu.Label inset>顯示方式</ContextMenu.Label>
<ContextMenu.RadioGroup value={view} onValueChange={setView}>
<ContextMenu.RadioItem value="grid">圖示</ContextMenu.RadioItem>
<ContextMenu.RadioItem value="list">清單</ContextMenu.RadioItem>
</ContextMenu.RadioGroup>
<ContextMenu.Separator />
<ContextMenu.Item variant="destructive">移到垃圾桶</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu>Props
ContextMenu
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
onOpenChange | (open: boolean) => void | — | 開啟/關閉時回呼。開啟位置來自右鍵事件,因此不提供受控的 open |
modal | boolean | true | 模態模式:開啟時攔截選單外的指標操作,點外面只會關閉選單、不會觸發底下的元素 |
children | ReactNode | — | ContextMenu.Trigger 與 ContextMenu.Content |
ContextMenu.Trigger
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
asChild | boolean | false | 把行為併入唯一的子元素,而不是額外包一層 div |
disabled | boolean | false | 停用時不攔截右鍵,瀏覽器原生選單照常出現 |
className | string | — | 併入觸發區塊;開啟時帶有 data-state="open" 可用來加樣式 |
其餘 HTML 屬性(aria-label、tabIndex 等)會透傳到觸發區塊;預設 tabIndex 為 0,讓鍵盤能聚焦後按 Shift+F10 開啟。
ContextMenu.Content
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
duration | number | 100 | 進出場動畫時長(ms),同時寫入 --wb-context-menu-duration |
subDelay | number | 150 | 子選單 hover 停留多久後展開(ms) |
collisionPadding | number | 8 | 與視窗邊緣保留的距離(px),放不下時翻轉或推移 |
loop | boolean | true | ↑/↓ 到頭尾時是否循環 |
aria-label | string | — | 選單的無障礙名稱 |
className | string | — | 併入選單面板 |
ContextMenu.Item
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
onSelect | (event: Event) => void | — | 選取時回呼;呼叫 event.preventDefault() 可讓選單保持開啟 |
disabled | boolean | false | 停用:設定 aria-disabled,並在方向鍵巡覽時略過 |
closeOnSelect | boolean | true | 選取後是否關閉選單 |
textValue | string | — | 首字跳轉比對用文字;預設取 children 的文字(略過 Shortcut) |
variant | "default" | "destructive" | "default" | destructive 為紅色的危險操作 |
inset | boolean | false | 左側留出指示器寬度,與核取/單選項對齊 |
className | string | — | 併入項目 |
ContextMenu.CheckboxItem
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
checked | boolean | — | 受控的勾選狀態 |
defaultChecked | boolean | false | 非受控模式的初始勾選狀態 |
onCheckedChange | (checked: boolean) => void | — | 勾選狀態改變時回呼 |
closeOnSelect | boolean | false | 選取後是否關閉選單 |
onSelect、disabled、textValue、className | — | — | 同 ContextMenu.Item |
ContextMenu.RadioGroup/RadioItem
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
value | string | — | RadioGroup:受控的選取值;RadioItem:此選項的值(必填) |
defaultValue | string | — | RadioGroup:非受控模式的初始選取值 |
onValueChange | (value: string) => void | — | RadioGroup:選取值改變時回呼 |
closeOnSelect | boolean | false | RadioItem:選取後是否關閉選單 |
其他
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
ContextMenu.Label | inset?、className? | — | 群組標題,非互動 |
ContextMenu.Group | aria-label?、className? | — | role="group" 的分組容器 |
ContextMenu.Separator | className? | — | role="separator" 的分隔線 |
ContextMenu.Shortcut | className? | — | 靠右的灰色等寬小字,只是標示,快捷鍵本身請自行綁定 |
ContextMenu.Sub | children | — | 包住 SubTrigger 與 SubContent |
ContextMenu.SubTrigger | disabled?、textValue?、inset?、className? | — | 開啟子選單的項目,右側自動加上箭頭 |
ContextMenu.SubContent | className? | — | 子選單面板,以 SubTrigger 為錨點 |
細節
- 定位:主選單的錨點是游標位置的 0×0 虛擬矩形,預設放在游標右下(
side右、align起點、間距 2px)。右側放不下翻到左側,下方放不下整塊往上推;transform-origin跟著游標相對浮層的位置,翻轉後依然從游標點長出來。子選單以 SubTrigger 為錨點,右側放不下翻到左側。浮層 portal 到body、position: fixed,並在視窗縮放、任何容器捲動與浮層尺寸變化時重新計算。 - 已開啟時在同一個 Trigger 內再按右鍵,選單會直接移到新位置,不重播完整的關閉+開啟。
- 觸控:
pointerType為 touch 的按壓持續 500ms 開啟;移動超過 10px 或抬起就取消。 - 動畫:進出場為淡入 +
scale(.95)+ 沿方向反側 8px 位移;duration驅動動畫並寫入--wb-context-menu-duration,面板內的轉場都讀這個變數,可在全域覆寫。高亮背景以共享版面動畫在項目間滑動。 - 子選單:hover 停留
subDelay後展開;指標從 SubTrigger 離開時,以離開點與子選單靠近側兩角構成的三角形為安全區,指標在區內移動時不會因經過其他項目而關閉子選單。 - 首字跳轉:500ms 內連續輸入的字元累加成前綴比對
textValue,中文以首字比對;同一個字元連按會在同字首項目間輪轉。輸入法組字中的按鍵一律略過。
可及性
- Content 為
role="menu"、aria-orientation="vertical";項目依種類為menuitem、menuitemcheckbox、menuitemradio,核取/單選項帶aria-checked - 開啟後焦點移到選單本身(不自動高亮第一項),按 ↓ 從第一項開始;↑/↓ 在非停用項間循環,Home/End 跳頭尾,Enter/Space 選取
- Escape 關閉並把焦點交回 Trigger;Tab 關閉選單,焦點從 Trigger 自然移往下一個元素
- SubTrigger 帶
aria-haspopup="menu"與aria-expanded;→ 或 Enter 開啟並聚焦子選單第一項,← 或 Escape 只關閉子選單並回到 SubTrigger - Trigger 聚焦時按 Shift+F10 或選單鍵,會在 Trigger 中心開啟選單
- 停用項設定
aria-disabled並在巡覽時略過;Shortcut 標示對輔助科技隱藏;分隔線為role="separator" - 使用者系統開啟「減少動態效果」時,進出場只保留淡入(時長不變),不縮放、不位移;高亮背景瞬間到位