Styling contract
Headless context menu styling stays on public slot and state hooks. The primitive does not position the panel for you, so owners usually pair those hooks with either anchored CSS or pointer-aware inline styles derived from the exported instance.
CSS contract table
| Selector | Applied on | Purpose |
|---|---|---|
[data-slot='context-menu-trigger'] | Context target surface | Right-click affordance, focus ring, and expanded target styling. |
[data-slot='menu'] | Context menu panel | Surface, placement, elevation, and open-state display rules. |
[data-slot='menu-item'] | Action row | Spacing, hover affordance, and keyboard active treatment. |
[data-slot='menu-item'][data-active] | Active action row | Reflects the arrow-key cursor managed by tngMenu. |
[data-slot='menu-group-label'], [data-slot='menu-separator'] | Menu sub-elements | Section labeling and separation for larger contextual action groups. |
Context-menu contract starter
css
.context-shell {
position: relative;
}
[data-slot="context-menu-trigger"] {
cursor: context-menu;
}
.context-shell [data-slot="menu"] {
position: absolute;
left: 0;
top: calc(100% + 0.42rem);
}
.context-shell [data-slot="menu"][hidden] {
display: none !important;
}
.context-shell [data-slot="menu-item"][data-active],
.context-shell [data-slot="menu-item"][aria-expanded="true"] {
background: color-mix(in srgb, var(--tng-semantic-accent-brand) 15%, transparent);
}
Pointer placement
For true cursor-relative placement, read getPointerAnchor() from the exported context-menu instance after open. Keyboard opens return null, so owners usually fall back to an element-relative placement rule.
Pointer-aware placement sketch
ts
readonly panelStyle = signal<Record<string, string>>({ position: 'absolute' });
syncPlacement(menu: TngContextMenu): void {
const pointerAnchor = menu.getPointerAnchor();
if (pointerAnchor !== null) {
this.panelStyle.set({
position: 'fixed',
left: `${pointerAnchor.x}px`,
top: `${pointerAnchor.y}px`,
});
return;
}
this.panelStyle.set({
position: 'absolute',
left: '0',
top: 'calc(100% + 0.42rem)',
});
}
State selectors
Style the trigger through aria-expanded, active rows through data-active, and submenu owners through aria-expanded on the item that owns a nested menu.
State selector starter
css
[data-slot="context-menu-trigger"][aria-expanded="true"] {
border-color: color-mix(in srgb, var(--tng-semantic-accent-brand) 45%, transparent);
}
[data-slot="menu-item"][data-active],
[data-slot="menu-item"][aria-expanded="true"] {
color: color-mix(
in srgb,
var(--tng-semantic-accent-brand) 72%,
var(--tng-semantic-foreground-primary)
);
}
[data-slot="menu-item"][aria-disabled="true"] {
cursor: default;
opacity: 0.55;
}