Context Menu
A menu that opens on a right click.
<contextmenu.ContextMenu id="demo-context" class="w-52">
<contextmenu.ContextMenuItem>
Copy
<contextmenu.ContextMenuShortcut>⌘C</contextmenu.ContextMenuShortcut>
</contextmenu.ContextMenuItem>
<contextmenu.ContextMenuItem>Cut</contextmenu.ContextMenuItem>
<contextmenu.ContextMenuSeparator />
<contextmenu.ContextMenuCheckboxItem name="bookmarks" checked>Show bookmarks</contextmenu.ContextMenuCheckboxItem>
<contextmenu.ContextMenuSeparator />
<contextmenu.ContextMenuRadioGroup label="People">
<contextmenu.ContextMenuLabel inset>People</contextmenu.ContextMenuLabel>
<contextmenu.ContextMenuRadioItem name="demo-context-person" value="ada" checked>
Ada
</contextmenu.ContextMenuRadioItem>
<contextmenu.ContextMenuRadioItem name="demo-context-person" value="grace">Grace</contextmenu.ContextMenuRadioItem>
</contextmenu.ContextMenuRadioGroup>
<contextmenu.ContextMenuSeparator />
<contextmenu.ContextMenuLink href={gx.URL("/docs")}>Docs</contextmenu.ContextMenuLink>
<contextmenu.ContextMenuItem variant={contextmenu.Destructive}>Delete</contextmenu.ContextMenuItem>
</contextmenu.ContextMenu>
<contextmenu.ContextMenuTrigger id="demo-context" class="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-border text-sm">
Right-click here
</contextmenu.ContextMenuTrigger>Installation
Run the command in the app module.
gx add context-menuThe command also installs icons.
The command writes these files.
| File | Path in the app |
|---|---|
ContextMenu.gx |
ui/context-menu/ContextMenu.gx |
ContextMenuCheckboxItem.gx |
ui/context-menu/ContextMenuCheckboxItem.gx |
ContextMenuGroup.gx |
ui/context-menu/ContextMenuGroup.gx |
ContextMenuItem.gx |
ui/context-menu/ContextMenuItem.gx |
ContextMenuLabel.gx |
ui/context-menu/ContextMenuLabel.gx |
ContextMenuLink.gx |
ui/context-menu/ContextMenuLink.gx |
ContextMenuRadioGroup.gx |
ui/context-menu/ContextMenuRadioGroup.gx |
ContextMenuRadioItem.gx |
ui/context-menu/ContextMenuRadioItem.gx |
ContextMenuSeparator.gx |
ui/context-menu/ContextMenuSeparator.gx |
ContextMenuShortcut.gx |
ui/context-menu/ContextMenuShortcut.gx |
ContextMenuSub.gx |
ui/context-menu/ContextMenuSub.gx |
ContextMenuSubContent.gx |
ui/context-menu/ContextMenuSubContent.gx |
ContextMenuSubTrigger.gx |
ui/context-menu/ContextMenuSubTrigger.gx |
ContextMenuTrigger.gx |
ui/context-menu/ContextMenuTrigger.gx |
ContextMenu.fixtures.go |
ui/context-menu/ContextMenu.fixtures.go |
ContextMenuCheckboxItem.fixtures.go |
ui/context-menu/ContextMenuCheckboxItem.fixtures.go |
ContextMenuGroup.fixtures.go |
ui/context-menu/ContextMenuGroup.fixtures.go |
ContextMenuItem.fixtures.go |
ui/context-menu/ContextMenuItem.fixtures.go |
ContextMenuLabel.fixtures.go |
ui/context-menu/ContextMenuLabel.fixtures.go |
ContextMenuLink.fixtures.go |
ui/context-menu/ContextMenuLink.fixtures.go |
ContextMenuRadioGroup.fixtures.go |
ui/context-menu/ContextMenuRadioGroup.fixtures.go |
ContextMenuRadioItem.fixtures.go |
ui/context-menu/ContextMenuRadioItem.fixtures.go |
ContextMenuSeparator.fixtures.go |
ui/context-menu/ContextMenuSeparator.fixtures.go |
ContextMenuShortcut.fixtures.go |
ui/context-menu/ContextMenuShortcut.fixtures.go |
ContextMenuSub.fixtures.go |
ui/context-menu/ContextMenuSub.fixtures.go |
ContextMenuSubContent.fixtures.go |
ui/context-menu/ContextMenuSubContent.fixtures.go |
ContextMenuSubTrigger.fixtures.go |
ui/context-menu/ContextMenuSubTrigger.fixtures.go |
ContextMenuTrigger.fixtures.go |
ui/context-menu/ContextMenuTrigger.fixtures.go |
styles.go |
ui/context-menu/styles.go |
Install icons first.
Copy each file to its path in the app. Change each import of a registry package to the path of that package in the app.
package contextmenu
props {
// Id is the id of the root element. The trigger opens the menu by this id.
Id string
// Class adds classes to the root element.
Class string = ""
// Children is the menu items.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<div id={p.Id} popover="auto" role="menu" data-gx-dismiss data-gx-roving="nowrap" data-gx-place="right start 2" class={gx.Cx("fixed z-50 m-0 min-w-[8rem] origin-top-left overflow-x-hidden overflow-y-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md outline-hidden opacity-0 scale-95 transition-[opacity,scale,translate,overlay,display] transition-discrete duration-150 open:opacity-100 open:scale-100 starting:open:opacity-0 starting:open:scale-95 starting:open:-translate-x-2 data-[side=left]:origin-top-right data-[side=left]:starting:open:translate-x-2 motion-reduce:transition-none", p.Class)} {...p.Attrs}>{p.Children}</div>package contextmenu
import "github.com/alternayte/gx/registry/icons"
props {
// Name is the name attribute of the checkbox input. A form or a signal reads it.
Name string = ""
// Value is the value that the form sends when the item is checked.
Value string = "on"
// Checked sets the item as checked on the first render.
Checked bool = false
// Disabled turns the item off. The arrow keys pass it.
Disabled bool = false
// Class adds classes to the root element.
Class string = ""
// Children is the label of the item.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<label class={gx.Cx("relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm select-none hover:bg-accent hover:text-accent-foreground has-[:focus]:bg-accent has-[:focus]:text-accent-foreground has-[:disabled]:pointer-events-none has-[:disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", p.Class)} {...p.Attrs}>
<input type="checkbox" role="menuitemcheckbox" tabindex="-1" name={p.Name} value={p.Value} checked={p.Checked} disabled={p.Disabled} class="peer sr-only" {...rovingItem(p.Disabled)} />
<span class="pointer-events-none invisible absolute left-2 flex size-3.5 items-center justify-center peer-checked:visible"><icons.Check class="size-4" /></span>
{p.Children}
</label>package contextmenu
props {
// Label is the accessible name of the group. It is not visible.
Label string = ""
// Class adds classes to the root element.
Class string = ""
// Children is the items of the group.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<div role="group" aria-label={p.Label} class={p.Class} {...p.Attrs}>{p.Children}</div>package contextmenu
props {
// Variant sets the visual style: Default or Destructive.
Variant Variant = Default
// Inset adds left padding, so the item lines up with the checkbox and radio items.
Inset bool = false
// Disabled turns the item off. The arrow keys pass it.
Disabled bool = false
// Class adds classes to the root element.
Class string = ""
// Children is the content of the item.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<button type="button" role="menuitem" tabindex="-1" disabled={p.Disabled} data-gx-close class={gx.Cx("relative flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm outline-hidden select-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", variantClass[p.variant()], insetClass[p.Inset], p.Class)} {...rovingItem(p.Disabled)} {...p.Attrs}>{p.Children}</button>package contextmenu
props {
// Inset adds left padding, so the label lines up with the checkbox and radio items.
Inset bool = false
// Class adds classes to the root element.
Class string = ""
// Children is the text of the label.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<div role="presentation" class={gx.Cx("px-2 py-1.5 text-sm font-medium text-foreground", insetClass[p.Inset], p.Class)} {...p.Attrs}>{p.Children}</div>package contextmenu
props {
// Href is the URL that the link opens.
Href gx.URL
// Class adds classes to the root element.
Class string = ""
// Children is the content of the link.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<a role="menuitem" tabindex="-1" href={p.Href} data-gx-roving-item data-gx-close class={gx.Cx("relative flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm no-underline outline-hidden select-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", p.Class)} {...p.Attrs}>{p.Children}</a>package contextmenu
props {
// Label is the accessible name of the group. It is not visible.
Label string = ""
// Class adds classes to the root element.
Class string = ""
// Children is the radio items of the group.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<div role="group" aria-label={p.Label} class={p.Class} {...p.Attrs}>{p.Children}</div>package contextmenu
import "github.com/alternayte/gx/registry/icons"
props {
// Name is the name attribute of the radio input. The radio items of one group share
// the same name.
Name string = ""
// Value is the value that the form sends when the item is checked.
Value string = ""
// Checked sets the item as checked on the first render.
Checked bool = false
// Disabled turns the item off. The arrow keys pass it.
Disabled bool = false
// Class adds classes to the root element.
Class string = ""
// Children is the label of the item.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<label class={gx.Cx("relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm select-none hover:bg-accent hover:text-accent-foreground has-[:focus]:bg-accent has-[:focus]:text-accent-foreground has-[:disabled]:pointer-events-none has-[:disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", p.Class)} {...p.Attrs}>
<input type="radio" role="menuitemradio" tabindex="-1" name={p.Name} value={p.Value} checked={p.Checked} disabled={p.Disabled} class="peer sr-only" {...rovingItem(p.Disabled)} />
<span class="pointer-events-none invisible absolute left-2 flex size-3.5 items-center justify-center peer-checked:visible"><icons.Circle class="size-2 *:fill-current" /></span>
{p.Children}
</label>package contextmenu
props {
// Class adds classes to the root element.
Class string = ""
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<div role="separator" class={gx.Cx("-mx-1 my-1 h-px bg-border", p.Class)} {...p.Attrs}></div>package contextmenu
props {
// Class adds classes to the root element.
Class string = ""
// Children is the text of the keyboard shortcut.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<span class={gx.Cx("ml-auto text-xs tracking-widest text-muted-foreground", p.Class)} {...p.Attrs}>{p.Children}</span>package contextmenu
props {
// Class adds classes to the root element.
Class string = ""
// Children is the sub-menu trigger, followed by the sub-menu content.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<div role="none" data-gx-sub class={p.Class} {...p.Attrs}>{p.Children}</div>package contextmenu
props {
// Class adds classes to the root element.
Class string = ""
// Children is the list of items of the sub-menu.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<div popover="auto" role="menu" data-gx-roving="nowrap" data-gx-place="right start" class={gx.Cx("z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg outline-hidden", subMotionClass, p.Class)} {...p.Attrs}>{p.Children}</div>package contextmenu
import "github.com/alternayte/gx/registry/icons"
props {
// Inset adds left padding, so the trigger lines up with the checkbox and radio items.
Inset bool = false
// Disabled turns the trigger off. The arrow keys pass it.
Disabled bool = false
// Class adds classes to the root element.
Class string = ""
// Children is the label of the trigger. A chevron follows it.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<button type="button" role="menuitem" tabindex="-1" aria-haspopup="menu" aria-expanded="false" disabled={p.Disabled} class={gx.Cx("flex w-full cursor-default items-center rounded-sm px-2 py-1.5 text-left text-sm outline-hidden select-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground aria-expanded:bg-accent aria-expanded:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", insetClass[p.Inset], p.Class)} {...rovingItem(p.Disabled)} {...p.Attrs}>
{p.Children}
<icons.ChevronRight class="ml-auto" />
</button>package contextmenu
props {
// Id is the id of the menu that a right click on the trigger opens.
Id string
// Class adds classes to the root element. The trigger has no style of its own.
Class string = ""
// Children is the content of the area that takes the right click.
Children gx.Node
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<div data-gx-contextmenu={"#" + p.Id} class={p.Class} {...p.Attrs}>{p.Children}</div>package contextmenu
import "github.com/alternayte/gx"
var ContextMenuFixtures = gx.Fixtures[ContextMenuProps]{
"Menu": {Id: "demo-context", Class: "w-52", Children: gx.Frag(
ContextMenuItem(ContextMenuItemProps{Children: gx.Frag(
gx.Text("Copy"),
ContextMenuShortcut(ContextMenuShortcutProps{Children: gx.Text("⌘C")}),
)}),
ContextMenuItem(ContextMenuItemProps{Children: gx.Text("Cut")}),
ContextMenuSeparator(ContextMenuSeparatorProps{}),
ContextMenuCheckboxItem(ContextMenuCheckboxItemProps{Name: "bookmarks", Checked: true, Children: gx.Text("Show bookmarks")}),
ContextMenuSeparator(ContextMenuSeparatorProps{}),
ContextMenuRadioGroup(ContextMenuRadioGroupProps{Label: "People", Children: gx.Frag(
ContextMenuLabel(ContextMenuLabelProps{Inset: true, Children: gx.Text("People")}),
ContextMenuRadioItem(ContextMenuRadioItemProps{Name: "demo-context-person", Value: "ada", Checked: true, Children: gx.Text("Ada")}),
ContextMenuRadioItem(ContextMenuRadioItemProps{Name: "demo-context-person", Value: "grace", Children: gx.Text("Grace")}),
)}),
ContextMenuSeparator(ContextMenuSeparatorProps{}),
ContextMenuLink(ContextMenuLinkProps{Href: gx.URL("/docs"), Children: gx.Text("Docs")}),
ContextMenuItem(ContextMenuItemProps{Variant: Destructive, Children: gx.Text("Delete")}),
)},
"Sub": {Id: "demo-context-sub", Class: "w-52", Children: gx.Frag(
ContextMenuItem(ContextMenuItemProps{Children: gx.Text("Back")}),
ContextMenuSub(ContextMenuSubProps{Children: gx.Frag(
ContextMenuSubTrigger(ContextMenuSubTriggerProps{Children: gx.Text("More tools")}),
ContextMenuSubContent(ContextMenuSubContentProps{Class: "w-48", Children: gx.Frag(
ContextMenuItem(ContextMenuItemProps{Children: gx.Text("Save page")}),
ContextMenuItem(ContextMenuItemProps{Children: gx.Text("Create shortcut")}),
ContextMenuSeparator(ContextMenuSeparatorProps{}),
ContextMenuItem(ContextMenuItemProps{Children: gx.Text("Developer tools")}),
)}),
)}),
ContextMenuSeparator(ContextMenuSeparatorProps{}),
ContextMenuItem(ContextMenuItemProps{Children: gx.Text("Reload")}),
)},
}package contextmenu
import "github.com/alternayte/gx"
var ContextMenuCheckboxItemFixtures = gx.Fixtures[ContextMenuCheckboxItemProps]{
"Checked": {Name: "status-bar", Checked: true, Children: gx.Text("Status bar")},
"Unchecked": {Name: "panel", Children: gx.Text("Panel")},
"Disabled": {Name: "activity", Disabled: true, Children: gx.Text("Activity bar")},
}
// ContextMenuCheckboxItemWrap renders the item inside a menu, as a page uses it.
func ContextMenuCheckboxItemWrap(n gx.Node) gx.Node {
return gx.El("div", gx.Attrs{{Key: "role", Value: "menu"}, {Key: "class", Value: "w-56"}}, n)
}package contextmenu
import "github.com/alternayte/gx"
var ContextMenuGroupFixtures = gx.Fixtures[ContextMenuGroupProps]{
"Default": {Label: "Account", Children: gx.Frag(
ContextMenuItem(ContextMenuItemProps{Children: gx.Text("Profile")}),
ContextMenuItem(ContextMenuItemProps{Children: gx.Text("Billing")}),
)},
}
// ContextMenuGroupWrap renders the group inside a menu, as a page uses it.
func ContextMenuGroupWrap(n gx.Node) gx.Node {
return gx.El("div", gx.Attrs{{Key: "role", Value: "menu"}, {Key: "class", Value: "w-56"}}, n)
}package contextmenu
import "github.com/alternayte/gx"
var ContextMenuItemFixtures = gx.Fixtures[ContextMenuItemProps]{
"Item": {Children: gx.Text("Profile")},
"Inset": {Inset: true, Children: gx.Text("Profile")},
"Destructive": {Variant: Destructive, Children: gx.Text("Delete")},
"Disabled": {Disabled: true, Children: gx.Text("Profile")},
}
// ContextMenuItemWrap renders the item inside a menu, as a page uses it.
func ContextMenuItemWrap(n gx.Node) gx.Node {
return gx.El("div", gx.Attrs{{Key: "role", Value: "menu"}, {Key: "class", Value: "w-56"}}, n)
}package contextmenu
import "github.com/alternayte/gx"
var ContextMenuLabelFixtures = gx.Fixtures[ContextMenuLabelProps]{
"Label": {Children: gx.Text("My account")},
"Inset": {Inset: true, Children: gx.Text("My account")},
}package contextmenu
import "github.com/alternayte/gx"
var ContextMenuLinkFixtures = gx.Fixtures[ContextMenuLinkProps]{"Link": {Href: gx.URL("/docs"), Children: gx.Text("Documentation")}}
// ContextMenuLinkWrap renders the item inside a menu, as a page uses it.
func ContextMenuLinkWrap(n gx.Node) gx.Node {
return gx.El("div", gx.Attrs{{Key: "role", Value: "menu"}, {Key: "class", Value: "w-56"}}, n)
}package contextmenu
import "github.com/alternayte/gx"
var ContextMenuRadioGroupFixtures = gx.Fixtures[ContextMenuRadioGroupProps]{
"Default": {Label: "Position", Children: gx.Frag(
ContextMenuRadioItem(ContextMenuRadioItemProps{Name: "context-menu-group-position", Value: "top", Checked: true, Children: gx.Text("Top")}),
ContextMenuRadioItem(ContextMenuRadioItemProps{Name: "context-menu-group-position", Value: "bottom", Children: gx.Text("Bottom")}),
)},
}
// ContextMenuRadioGroupWrap renders the group inside a menu, as a page uses it.
func ContextMenuRadioGroupWrap(n gx.Node) gx.Node {
return gx.El("div", gx.Attrs{{Key: "role", Value: "menu"}, {Key: "class", Value: "w-56"}}, n)
}package contextmenu
import "github.com/alternayte/gx"
var ContextMenuRadioItemFixtures = gx.Fixtures[ContextMenuRadioItemProps]{
"Checked": {Name: "context-menu-item-checked", Value: "top", Checked: true, Children: gx.Text("Top")},
"Unchecked": {Name: "context-menu-item-unchecked", Value: "bottom", Children: gx.Text("Bottom")},
}
// ContextMenuRadioItemWrap renders the item inside a menu, as a page uses it.
func ContextMenuRadioItemWrap(n gx.Node) gx.Node {
return gx.El("div", gx.Attrs{{Key: "role", Value: "menu"}, {Key: "class", Value: "w-56"}}, n)
}package contextmenu
import "github.com/alternayte/gx"
var ContextMenuSeparatorFixtures = gx.Fixtures[ContextMenuSeparatorProps]{"Default": {}}package contextmenu
import "github.com/alternayte/gx"
var ContextMenuShortcutFixtures = gx.Fixtures[ContextMenuShortcutProps]{"Default": {Children: gx.Text("⌘K")}}package contextmenu
import "github.com/alternayte/gx"
var ContextMenuSubFixtures = gx.Fixtures[ContextMenuSubProps]{
"Default": {Children: gx.Frag(
ContextMenuSubTrigger(ContextMenuSubTriggerProps{Children: gx.Text("More tools")}),
ContextMenuSubContent(ContextMenuSubContentProps{Children: gx.Frag(
ContextMenuItem(ContextMenuItemProps{Children: gx.Text("Save page")}),
ContextMenuItem(ContextMenuItemProps{Children: gx.Text("Create shortcut")}),
)}),
)},
}
// ContextMenuSubWrap renders the sub-menu inside a menu, as a page uses it.
func ContextMenuSubWrap(n gx.Node) gx.Node {
return gx.El("div", gx.Attrs{{Key: "role", Value: "menu"}, {Key: "class", Value: "w-56"}}, n)
}package contextmenu
import "github.com/alternayte/gx"
var ContextMenuSubContentFixtures = gx.Fixtures[ContextMenuSubContentProps]{
"Content": {Class: "w-48", Children: gx.Frag(
ContextMenuItem(ContextMenuItemProps{Children: gx.Text("Save page")}),
ContextMenuItem(ContextMenuItemProps{Children: gx.Text("Create shortcut")}),
ContextMenuSeparator(ContextMenuSeparatorProps{}),
ContextMenuItem(ContextMenuItemProps{Children: gx.Text("Developer tools")}),
)},
}
// ContextMenuSubContentWrap renders the content behind its trigger inside a menu,
// as a page uses it.
func ContextMenuSubContentWrap(n gx.Node) gx.Node {
return gx.El("div", gx.Attrs{{Key: "role", Value: "menu"}, {Key: "class", Value: "w-56"}},
ContextMenuSub(ContextMenuSubProps{Children: gx.Frag(
ContextMenuSubTrigger(ContextMenuSubTriggerProps{Children: gx.Text("More tools")}),
n,
)}),
)
}package contextmenu
import "github.com/alternayte/gx"
var ContextMenuSubTriggerFixtures = gx.Fixtures[ContextMenuSubTriggerProps]{
"Trigger": {Children: gx.Text("More tools")},
"Inset": {Inset: true, Children: gx.Text("More tools")},
"Disabled": {Disabled: true, Children: gx.Text("More tools")},
}
// ContextMenuSubTriggerWrap renders the trigger inside a menu, as a page uses it.
func ContextMenuSubTriggerWrap(n gx.Node) gx.Node {
return gx.El("div", gx.Attrs{{Key: "role", Value: "menu"}, {Key: "class", Value: "w-56"}}, n)
}package contextmenu
import "github.com/alternayte/gx"
var ContextMenuTriggerFixtures = gx.Fixtures[ContextMenuTriggerProps]{
"Default": {
Id: "demo-context",
Class: "flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-border text-sm",
Children: gx.Text("Right-click here"),
},
"Sub": {
Id: "demo-context-sub",
Class: "flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-border text-sm",
Children: gx.Text("Right-click for a sub-menu"),
},
}package contextmenu
import "github.com/alternayte/gx"
// Variant is the visual style of a menu item.
type Variant string
// The variants of contextmenu.ContextMenuItem.
const (
Default Variant = "default"
Destructive Variant = "destructive"
)
var variantClass = gx.Enum[Variant]{
Default: "",
Destructive: "text-destructive hover:bg-destructive/10 hover:text-destructive focus:bg-destructive/10 focus:text-destructive dark:hover:bg-destructive/20 dark:focus:bg-destructive/20 *:[svg]:text-destructive!",
}
// variant returns the variant of one item; a zero value is Default.
func (p ContextMenuItemProps) variant() Variant {
if p.Variant == "" {
return Default
}
return p.Variant
}
// insetClass lines an item or a label up with the checkbox and radio items.
var insetClass = map[bool]string{
true: "pl-8",
false: "",
}
// rovingItem marks an item for the roving tabindex of its menu. A disabled
// item is not marked, so the arrow keys pass it.
func rovingItem(disabled bool) gx.Attrs {
return gx.Attrs{gx.Bool("data-gx-roving-item", !disabled)}
}
// subMotionClass fades and zooms the content of a sub-menu from 95% and
// slides it 2 units from its trigger. The overlay module writes data-side:
// content that flips to the left slides from the right.
const subMotionClass = "origin-top-left opacity-0 scale-95 transition-[opacity,scale,translate,overlay,display] transition-discrete duration-150 open:opacity-100 open:scale-100 starting:open:opacity-0 starting:open:scale-95 starting:open:-translate-x-2 data-[side=left]:origin-top-right data-[side=left]:starting:open:translate-x-2 motion-reduce:transition-none"The theme must define these tokens: --border, --popover, --popover-foreground, --foreground, --accent, --accent-foreground, --muted-foreground, --destructive.
Usage
<contextmenu.ContextMenuTrigger id="row-menu" class="rounded-md border border-dashed p-8">Right-click here</contextmenu.ContextMenuTrigger>
<contextmenu.ContextMenu id="row-menu" class="w-52">
<contextmenu.ContextMenuItem>
Copy
<contextmenu.ContextMenuShortcut>⌘C</contextmenu.ContextMenuShortcut>
</contextmenu.ContextMenuItem>
<contextmenu.ContextMenuSeparator />
<contextmenu.ContextMenuCheckboxItem name="bookmarks" checked={true}>Show bookmarks</contextmenu.ContextMenuCheckboxItem>
<contextmenu.ContextMenuRadioGroup label="People">
<contextmenu.ContextMenuLabel inset={true}>People</contextmenu.ContextMenuLabel>
<contextmenu.ContextMenuRadioItem name="person" value="ada" checked={true}>Ada</contextmenu.ContextMenuRadioItem>
<contextmenu.ContextMenuRadioItem name="person" value="grace">Grace</contextmenu.ContextMenuRadioItem>
</contextmenu.ContextMenuRadioGroup>
<contextmenu.ContextMenuSeparator />
<contextmenu.ContextMenuSub>
<contextmenu.ContextMenuSubTrigger>More tools</contextmenu.ContextMenuSubTrigger>
<contextmenu.ContextMenuSubContent>
<contextmenu.ContextMenuItem>Save page</contextmenu.ContextMenuItem>
<contextmenu.ContextMenuItem>Developer tools</contextmenu.ContextMenuItem>
</contextmenu.ContextMenuSubContent>
</contextmenu.ContextMenuSub>
<contextmenu.ContextMenuLink href={gx.URL("/docs")}>Docs</contextmenu.ContextMenuLink>
<contextmenu.ContextMenuItem variant={contextmenu.Destructive}>Delete</contextmenu.ContextMenuItem>
</contextmenu.ContextMenu>The trigger is an area with no style of its own; Class gives it one. The menu opens at the pointer and focus moves to the first item. It opens to the left of the pointer when the right side is too small, and it shifts up to stay 8px inside the viewport. data-side on the menu names the side it took.
A checkbox item and a radio item are native inputs. They carry Name and Value, so a form or a signal reads them. They keep the menu open. An item has Inset, Disabled and the Destructive variant.
The menu fades and zooms in and out. A user who asks for reduced motion gets no transition.
ContextMenuSub holds one ContextMenuSubTrigger and one ContextMenuSubContent, in that order. The sub-menu opens to the right of its trigger, or to the left when the right side is too small. It opens on a click and after the pointer rests on the trigger. It stays open while the pointer moves toward it. An item of a sub-menu closes the whole menu. The trigger has Inset and Disabled.
Examples
ContextMenu: Menu
<contextmenu.ContextMenu id="demo-context" class="w-52">
<contextmenu.ContextMenuItem>
Copy
<contextmenu.ContextMenuShortcut>⌘C</contextmenu.ContextMenuShortcut>
</contextmenu.ContextMenuItem>
<contextmenu.ContextMenuItem>Cut</contextmenu.ContextMenuItem>
<contextmenu.ContextMenuSeparator />
<contextmenu.ContextMenuCheckboxItem name="bookmarks" checked>Show bookmarks</contextmenu.ContextMenuCheckboxItem>
<contextmenu.ContextMenuSeparator />
<contextmenu.ContextMenuRadioGroup label="People">
<contextmenu.ContextMenuLabel inset>People</contextmenu.ContextMenuLabel>
<contextmenu.ContextMenuRadioItem name="demo-context-person" value="ada" checked>
Ada
</contextmenu.ContextMenuRadioItem>
<contextmenu.ContextMenuRadioItem name="demo-context-person" value="grace">Grace</contextmenu.ContextMenuRadioItem>
</contextmenu.ContextMenuRadioGroup>
<contextmenu.ContextMenuSeparator />
<contextmenu.ContextMenuLink href={gx.URL("/docs")}>Docs</contextmenu.ContextMenuLink>
<contextmenu.ContextMenuItem variant={contextmenu.Destructive}>Delete</contextmenu.ContextMenuItem>
</contextmenu.ContextMenu>
<contextmenu.ContextMenuTrigger id="demo-context" class="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-border text-sm">
Right-click here
</contextmenu.ContextMenuTrigger>ContextMenu: Sub
<contextmenu.ContextMenu id="demo-context-sub" class="w-52">
<contextmenu.ContextMenuItem>Back</contextmenu.ContextMenuItem>
<contextmenu.ContextMenuSub>
<contextmenu.ContextMenuSubTrigger>More tools</contextmenu.ContextMenuSubTrigger>
<contextmenu.ContextMenuSubContent class="w-48">
<contextmenu.ContextMenuItem>Save page</contextmenu.ContextMenuItem>
<contextmenu.ContextMenuItem>Create shortcut</contextmenu.ContextMenuItem>
<contextmenu.ContextMenuSeparator />
<contextmenu.ContextMenuItem>Developer tools</contextmenu.ContextMenuItem>
</contextmenu.ContextMenuSubContent>
</contextmenu.ContextMenuSub>
<contextmenu.ContextMenuSeparator />
<contextmenu.ContextMenuItem>Reload</contextmenu.ContextMenuItem>
</contextmenu.ContextMenu>
<contextmenu.ContextMenuTrigger id="demo-context-sub" class="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed border-border text-sm">
Right-click for a sub-menu
</contextmenu.ContextMenuTrigger>ContextMenuCheckboxItem: Checked
<contextmenu.ContextMenuCheckboxItem name="status-bar" checked>Status bar</contextmenu.ContextMenuCheckboxItem>ContextMenuCheckboxItem: Unchecked
<contextmenu.ContextMenuCheckboxItem name="panel">Panel</contextmenu.ContextMenuCheckboxItem>ContextMenuCheckboxItem: Disabled
<contextmenu.ContextMenuCheckboxItem name="activity" disabled>Activity bar</contextmenu.ContextMenuCheckboxItem>ContextMenuGroup: Default
<contextmenu.ContextMenuGroup label="Account">
<contextmenu.ContextMenuItem>Profile</contextmenu.ContextMenuItem>
<contextmenu.ContextMenuItem>Billing</contextmenu.ContextMenuItem>
</contextmenu.ContextMenuGroup>ContextMenuItem: Item
<contextmenu.ContextMenuItem>Profile</contextmenu.ContextMenuItem>ContextMenuItem: Inset
<contextmenu.ContextMenuItem inset>Profile</contextmenu.ContextMenuItem>ContextMenuItem: Destructive
<contextmenu.ContextMenuItem variant={contextmenu.Destructive}>Delete</contextmenu.ContextMenuItem>ContextMenuItem: Disabled
<contextmenu.ContextMenuItem disabled>Profile</contextmenu.ContextMenuItem>ContextMenuLabel: Label
<contextmenu.ContextMenuLabel>My account</contextmenu.ContextMenuLabel>ContextMenuLabel: Inset
<contextmenu.ContextMenuLabel inset>My account</contextmenu.ContextMenuLabel>ContextMenuLink: Link
<contextmenu.ContextMenuLink href={gx.URL("/docs")}>Documentation</contextmenu.ContextMenuLink>ContextMenuRadioGroup: Default
<contextmenu.ContextMenuRadioGroup label="Position">
<contextmenu.ContextMenuRadioItem name="context-menu-group-position" value="top" checked>
Top
</contextmenu.ContextMenuRadioItem>
<contextmenu.ContextMenuRadioItem name="context-menu-group-position" value="bottom">
Bottom
</contextmenu.ContextMenuRadioItem>
</contextmenu.ContextMenuRadioGroup>ContextMenuRadioItem: Checked
<contextmenu.ContextMenuRadioItem name="context-menu-item-checked" value="top" checked>
Top
</contextmenu.ContextMenuRadioItem>ContextMenuRadioItem: Unchecked
<contextmenu.ContextMenuRadioItem name="context-menu-item-unchecked" value="bottom">
Bottom
</contextmenu.ContextMenuRadioItem>ContextMenuSeparator: Default
<contextmenu.ContextMenuSeparator />ContextMenuShortcut: Default
<contextmenu.ContextMenuShortcut>⌘K</contextmenu.ContextMenuShortcut>ContextMenuSub: Default
<contextmenu.ContextMenuSub>
<contextmenu.ContextMenuSubTrigger>More tools</contextmenu.ContextMenuSubTrigger>
<contextmenu.ContextMenuSubContent>
<contextmenu.ContextMenuItem>Save page</contextmenu.ContextMenuItem>
<contextmenu.ContextMenuItem>Create shortcut</contextmenu.ContextMenuItem>
</contextmenu.ContextMenuSubContent>
</contextmenu.ContextMenuSub>ContextMenuSubContent: Content
<contextmenu.ContextMenuSubContent class="w-48">
<contextmenu.ContextMenuItem>Save page</contextmenu.ContextMenuItem>
<contextmenu.ContextMenuItem>Create shortcut</contextmenu.ContextMenuItem>
<contextmenu.ContextMenuSeparator />
<contextmenu.ContextMenuItem>Developer tools</contextmenu.ContextMenuItem>
</contextmenu.ContextMenuSubContent>ContextMenuSubTrigger: Trigger
<contextmenu.ContextMenuSubTrigger>More tools</contextmenu.ContextMenuSubTrigger>ContextMenuSubTrigger: Inset
<contextmenu.ContextMenuSubTrigger inset>More tools</contextmenu.ContextMenuSubTrigger>ContextMenuSubTrigger: Disabled
<contextmenu.ContextMenuSubTrigger disabled>More tools</contextmenu.ContextMenuSubTrigger>API reference
A tag sets a prop by its name with a lower-case first letter: Class is class.
contextmenu.ContextMenu
| Prop | Type | Default | Description |
|---|---|---|---|
Id |
string |
Required | Id is the id of the root element. The trigger opens the menu by this id. |
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the menu items. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
contextmenu.ContextMenuCheckboxItem
| Prop | Type | Default | Description |
|---|---|---|---|
Name |
string |
"" |
Name is the name attribute of the checkbox input. A form or a signal reads it. |
Value |
string |
"on" |
Value is the value that the form sends when the item is checked. |
Checked |
bool |
false |
Checked sets the item as checked on the first render. |
Disabled |
bool |
false |
Disabled turns the item off. The arrow keys pass it. |
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the label of the item. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
contextmenu.ContextMenuGroup
| Prop | Type | Default | Description |
|---|---|---|---|
Label |
string |
"" |
Label is the accessible name of the group. It is not visible. |
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the items of the group. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
contextmenu.ContextMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
Variant |
Variant |
Default |
Variant sets the visual style: Default or Destructive. |
Inset |
bool |
false |
Inset adds left padding, so the item lines up with the checkbox and radio items. |
Disabled |
bool |
false |
Disabled turns the item off. The arrow keys pass it. |
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the content of the item. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
contextmenu.ContextMenuLabel
| Prop | Type | Default | Description |
|---|---|---|---|
Inset |
bool |
false |
Inset adds left padding, so the label lines up with the checkbox and radio items. |
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the text of the label. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
contextmenu.ContextMenuLink
| Prop | Type | Default | Description |
|---|---|---|---|
Href |
gx.URL |
Required | Href is the URL that the link opens. |
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the content of the link. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
contextmenu.ContextMenuRadioGroup
| Prop | Type | Default | Description |
|---|---|---|---|
Label |
string |
"" |
Label is the accessible name of the group. It is not visible. |
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the radio items of the group. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
contextmenu.ContextMenuRadioItem
| Prop | Type | Default | Description |
|---|---|---|---|
Name |
string |
"" |
Name is the name attribute of the radio input. The radio items of one group share the same name. |
Value |
string |
"" |
Value is the value that the form sends when the item is checked. |
Checked |
bool |
false |
Checked sets the item as checked on the first render. |
Disabled |
bool |
false |
Disabled turns the item off. The arrow keys pass it. |
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the label of the item. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
contextmenu.ContextMenuSeparator
| Prop | Type | Default | Description |
|---|---|---|---|
Class |
string |
"" |
Class adds classes to the root element. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
contextmenu.ContextMenuShortcut
| Prop | Type | Default | Description |
|---|---|---|---|
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the text of the keyboard shortcut. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
contextmenu.ContextMenuSub
| Prop | Type | Default | Description |
|---|---|---|---|
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the sub-menu trigger, followed by the sub-menu content. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
contextmenu.ContextMenuSubContent
| Prop | Type | Default | Description |
|---|---|---|---|
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the list of items of the sub-menu. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
contextmenu.ContextMenuSubTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
Inset |
bool |
false |
Inset adds left padding, so the trigger lines up with the checkbox and radio items. |
Disabled |
bool |
false |
Disabled turns the trigger off. The arrow keys pass it. |
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
Required | Children is the label of the trigger. A chevron follows it. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
contextmenu.ContextMenuTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
Id |
string |
Required | Id is the id of the menu that a right click on the trigger opens. |
Class |
string |
"" |
Class adds classes to the root element. The trigger has no style of its own. |
Children |
gx.Node |
Required | Children is the content of the area that takes the right click. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
Do and do not
Do
- Attach the menu to the object it acts on.
- Keep the same items as the visible row menu.
- Give the radio items of one group the same
Name.
Don't
- Do not hide the only action behind a right click.
- Do not open a context menu on a whole page.
- Do not put an element between the trigger and the content of a sub-menu.
Keyboard
| Key | Action |
|---|---|
| Context menu key, Shift+F10 | Opens the menu at the focused element of the area. |
| Up, Down | Moves through the items. A disabled item is passed. |
| Home, End | Moves to the first or last item. |
| A letter | Moves to the next item that starts with the typed text. |
| Enter | Runs the focused item and closes the menu. Toggles a checkbox or radio item. |
| Space | Runs the focused item. Toggles a checkbox or radio item. |
| Right, Enter, Space | On a sub-menu trigger, opens the sub-menu and moves to its first item. |
| Left, Escape | In a sub-menu, closes it and returns focus to its trigger. |
| Escape | Closes the menu. |