Slash Menu
<zn-slash-menu> | ZnSlashMenu
A keyboard-driven list of insertions, anchored to the caret of the field that opened it.
<zn-slash-menu> is the panel behind a slash menu. Most of the time you don’t use it
directly — a component drives it for you, as
zn-textarea does with its
zn-slash-item entries:
<zn-textarea label="Terms and conditions" rows="5" help-text="Type / to insert a replacement string" slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}"></zn-textarea>
Slot one into a textarea when you want the panel’s own settings — heading,
max-items, placement, empty-text, or its width — declared in markup.
The textarea then drives your menu instead of building its own:
<zn-textarea label="Terms and conditions" rows="6" help-text="Type / to insert"> <zn-slash-menu slot="slash-menu" heading="Replacement strings" max-items="6" style="--slash-menu-width: 360px"> <zn-slash-item icon="tag@lu" label="Brand name" value="{{BRAND_NAME}}"></zn-slash-item> <zn-slash-item icon="building@lu" label="Legal entity" value="{{LEGAL_ENTITY}}"></zn-slash-item> <zn-slash-item icon="scale@lu" label="Jurisdiction" value="{{JURISDICTION}}"></zn-slash-item> </zn-slash-menu> </zn-textarea>
Reach for the component on its own when you are adding a slash menu to a control the library doesn’t cover.
It renders and positions the list; the SlashMenuController watches a text field, tracks the
query and inserts the result.
Examples
Driving It Directly
Set items, position the panel with anchor (an element or a
virtual element), and call show(). The menu emits zn-slash-item-select when an item is chosen.
That event does not cross shadow boundaries, so a component that hosts the menu in its shadow root re-emits
it as its own public event.
<zn-button id="menu-anchor">Open the menu</zn-button> <zn-slash-menu id="standalone-menu"></zn-slash-menu> <div id="standalone-log" style="margin-top: 1rem; font-family: monospace; font-size: 0.875rem;"></div> <script type="module"> const anchor = document.getElementById('menu-anchor'); const menu = document.getElementById('standalone-menu'); const log = document.getElementById('standalone-log'); await customElements.whenDefined('zn-slash-menu'); menu.items = [ {label: 'Brand name', value: '{{BRAND_NAME}}', icon: 'tag@lu', group: 'Merchant'}, {label: 'Legal entity', value: '{{LEGAL_ENTITY}}', icon: 'building@lu', group: 'Merchant'}, {label: 'Jurisdiction', value: '{{JURISDICTION}}', icon: 'scale@lu', group: 'Policy'} ]; menu.anchor = anchor; anchor.addEventListener('click', () => menu.open ? menu.hide() : menu.show()); menu.addEventListener('zn-slash-item-select', (event) => { log.textContent = `selected ${event.detail.item.label} → ${event.detail.item.value}`; menu.hide(); }); </script>
Keyboard Navigation
The menu doesn’t listen for keys itself — whatever owns the field decides which keys belong to the menu,
then calls moveActive(), selectActive() and hide(). Disabled items
are skipped, and moving past either end wraps around.
<zn-button id="nav-toggle">Toggle</zn-button> <zn-button id="nav-up">↑</zn-button> <zn-button id="nav-down">↓</zn-button> <zn-button id="nav-select">Enter</zn-button> <zn-slash-menu id="nav-menu"></zn-slash-menu> <div id="nav-log" style="margin-top: 1rem; font-family: monospace; font-size: 0.875rem;"></div> <script type="module"> const menu = document.getElementById('nav-menu'); const toggle = document.getElementById('nav-toggle'); const log = document.getElementById('nav-log'); await customElements.whenDefined('zn-slash-menu'); menu.items = [ {label: 'Brand name', value: '{{BRAND_NAME}}'}, {label: 'Not available here', value: '{{INVOICE_NUMBER}}', disabled: true}, {label: 'Jurisdiction', value: '{{JURISDICTION}}'} ]; menu.anchor = toggle; toggle.addEventListener('click', () => menu.open ? menu.hide() : menu.show()); document.getElementById('nav-up').addEventListener('click', () => menu.moveActive(-1)); document.getElementById('nav-down').addEventListener('click', () => menu.moveActive(1)); document.getElementById('nav-select').addEventListener('click', () => menu.selectActive()); menu.addEventListener('zn-slash-item-select', (event) => { log.textContent = `selected ${event.detail.item.label}`; }); </script>
Truncating Long Lists
max-items caps how many items are rendered; the rest are reported in a footer rather than
silently dropped. The panel scrolls when its content exceeds --slash-menu-max-height.
<zn-textarea label="Terms and conditions" rows="5" help-text="Type / to see 3 of 9, then keep typing to narrow" slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}, Jurisdiction={{JURISDICTION}}, Customer name={{CUSTOMER_NAME}}, Customer email={{CUSTOMER_EMAIL}}, Invoice number={{INVOICE_NUMBER}}, Invoice date={{INVOICE_DATE}}, Support email={{SUPPORT_EMAIL}}, Support phone={{SUPPORT_PHONE}}"> <zn-slash-menu slot="slash-menu" heading="Replacement strings" max-items="3"></zn-slash-menu> </zn-textarea>
Attaching It To Your Own Field
SlashMenuController is the reusable half. Give it the field, the menu, and the items; it
handles trigger detection, filtering, keyboard handling and insertion.
import {SlashMenuController} from '@kubex/zinc'; class MyEditor extends ZincElement { private slash = new SlashMenuController(this, { menu: () => this.shadowRoot.querySelector('zn-slash-menu'), items: () => [{label: 'Brand name', value: '{{BRAND_NAME}}', icon: 'tag@lu'}], trigger: () => '/', onSelect: (item, query) => !this.emit('my-select', {detail: {item, query}}).defaultPrevented, onInsert: (item, value) => this.emit('my-insert', {detail: {item, value}}) }); firstUpdated() { this.slash.attach(this.shadowRoot.querySelector('textarea')); } }
Items can be shared between fields by registering them once as a preset:
import {registerSlashMenuPreset} from '@kubex/zinc'; registerSlashMenuPreset('legal', [ {label: 'Brand name', value: '{{BRAND_NAME}}', icon: 'tag@lu'}, {label: 'Jurisdiction', value: '{{JURISDICTION}}', icon: 'scale@lu'} ]);
Any component that reads presets — zn-textarea via slash-preset="legal" — then
offers the same list.
Importing
If you’re using the autoloader or the traditional loader, you can ignore this section. Otherwise, feel free to use any of the following snippets to cherry pick this component.
To import this component from the CDN using a script tag:
<script type="module" src="https://cdn.jsdelivr.net/npm/@kubex/zinc@1.1.82/dist/components/slash-menu/slash-menu.js"></script>
To import this component from the CDN using a JavaScript import:
import 'https://cdn.jsdelivr.net/npm/@kubex/zinc@1.1.82/dist/components/slash-menu/slash-menu.js';
To import this component using a bundler:
import '@kubex/zinc/dist/components/slash-menu/slash-menu.js';
Properties
| Name | Description | Reflects | Type | Default |
|---|---|---|---|---|
open
|
Whether the menu is showing. |
|
boolean
|
false
|
items
|
The items to list. Already filtered — the menu displays what it is given. |
SlashMenuItem[]
|
[]
|
|
query
|
The query the items were matched against, shown in the heading. |
string
|
''
|
|
heading
|
The heading shown when there is no query. |
string
|
'Insert'
|
|
emptyText
empty-text
|
Shown in place of the list when there are no items. |
string
|
'No matches'
|
|
maxItems
max-items
|
The most items to render at once. Remaining matches are reported in the footer. |
number
|
25
|
|
anchor
|
The element or caret rect the panel is positioned against. |
Element | VirtualElement | null
|
null
|
|
placement
|
The preferred placement of the panel. |
Placement
|
'bottom-start'
|
|
distance
|
The gap between the caret and the panel. |
number
|
4
|
|
activeItem
|
The item that Enter would insert. |
SlashMenuItem | undefined
|
- | |
updateComplete |
A read-only promise that resolves when the component has finished updating. |
Learn more about attributes and properties.
Events
| Name | React Event | Description | Event Detail |
|---|---|---|---|
SLASH_ITEM_SELECT |
|
CustomEvent
|
|
zn-slash-item-select |
|
Emitted when an item is chosen. Does not cross shadow boundaries; the component driving the menu
(e.g. zn-textarea) re-emits it as zn-slash-select.
|
- |
Learn more about events.
Methods
| Name | Description | Arguments |
|---|---|---|
setActiveIndex() |
Sets the active item by index, wrapping at both ends and skipping disabled items. |
index: number
|
moveActive() |
Moves the active item by delta places. |
delta: number
|
selectActive() |
Chooses the active item, as pressing Enter would. | - |
reposition() |
Recalculates the panel’s position against its anchor. | - |
Learn more about methods.
Custom Properties
| Name | Description | Default |
|---|---|---|
--slash-menu-width |
The width of the panel. | |
--slash-menu-max-height |
The maximum height of the panel before it scrolls. |
Learn more about customizing CSS custom properties.
Parts
| Name | Description |
|---|---|
panel |
The floating panel that holds the list. |
heading |
The panel’s heading. |
item |
An item in the list. |
group-heading |
A group heading between items. |
footer |
The truncation footer, shown when not every match fits. |
Learn more about customizing CSS parts.
Dependencies
This component automatically imports the following dependencies.
<zn-example><zn-icon>