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 — max-items,
placement, empty-text, its width, or the heading the list is
announced by — 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 group="Merchant" icon="tag@lu" label="Brand name" value="{{BRAND_NAME}}"></zn-slash-item> <zn-slash-item group="Merchant" icon="building@lu" label="Legal entity" value="{{LEGAL_ENTITY}}"></zn-slash-item> <zn-slash-item group="Policy" 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>
Grouping Items
An item’s group puts a heading above it. The heading is drawn whenever the group changes going
down the list, so items sharing a group must be declared together — the menu lists them in the order it is
given rather than gathering them for you. Leave the group off and an item is listed under no heading at all;
declared first, those lead the list, which is how a handful of favourites can sit above named sections.
<zn-textarea label="Privacy policy" rows="7" help-text="Type / to see the sections, or 'company' to search across them"> <zn-slash-menu slot="slash-menu" style="--slash-menu-width: 340px"> <zn-slash-item icon="star@lu" label="Merchant block" value="{{MERCHANT_BLOCK}}"></zn-slash-item> <zn-slash-item group="Merchant" icon="tag@lu" label="Brand name" keywords="company" value="{{BRAND_NAME}}"></zn-slash-item> <zn-slash-item group="Merchant" icon="building@lu" label="Legal entity" keywords="company" value="{{LEGAL_ENTITY}}"></zn-slash-item> <zn-slash-item group="Customer" icon="user@lu" label="Customer name" value="{{CUSTOMER_NAME}}"></zn-slash-item> <zn-slash-item group="Customer" icon="mail@lu" label="Customer email" value="{{CUSTOMER_EMAIL}}"></zn-slash-item> <zn-slash-item group="Policy" icon="scale@lu" label="Jurisdiction" value="{{JURISDICTION}}"></zn-slash-item> <zn-slash-item group="Policy" icon="calendar@lu" label="Refund window" value="{{REFUND_DAYS}} days"></zn-slash-item> </zn-slash-menu> </zn-textarea>
Group headings are only a structure for browsing: a query ranks every match on merit, and the headings
follow whatever order that leaves. Set order on an item to pin its place within a match band,
and use keywords to make it findable by terms that aren’t in its label.
Driving the menu yourself, the same thing is a group on each item:
<zn-button id="grouped-anchor">Open the menu</zn-button> <zn-slash-menu id="grouped-menu"></zn-slash-menu> <div id="grouped-log" style="margin-top: 1rem; font-family: monospace; font-size: 0.875rem;"></div> <script type="module"> const anchor = document.getElementById('grouped-anchor'); const menu = document.getElementById('grouped-menu'); const log = document.getElementById('grouped-log'); await customElements.whenDefined('zn-slash-menu'); menu.items = [ {label: 'Paragraph', value: '', icon: 'type@lu', group: 'Basic blocks'}, {label: 'Heading 1', value: '# ', icon: 'heading-1@lu', group: 'Basic blocks'}, {label: 'Heading 2', value: '## ', icon: 'heading-2@lu', group: 'Basic blocks'}, {label: 'To-do list', value: '- [ ] ', icon: 'square-check@lu', group: 'Lists'}, {label: 'Bulleted list', value: '- ', icon: 'list@lu', group: 'Lists'}, {label: 'Callout', value: 'NOTE: ', icon: 'info@lu', group: 'Advanced'}, {label: 'Quote', value: '> ', icon: 'quote@lu', group: 'Advanced'} ]; menu.hideKeys = true; 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} from ${event.detail.item.group}`; menu.hide(); }); </script>
Keyboard Hints
A footer pinned to the bottom of the panel spells out the keys the menu answers to. The list scrolls beneath
it, so the hints stay in view. Use hide-hints on menus driven entirely by the mouse, or where
the surrounding UI already explains the shortcuts.
<zn-textarea label="Terms and conditions" rows="4" help-text="Type / to see the hints" slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}, Jurisdiction={{JURISDICTION}}"> <zn-slash-menu slot="slash-menu" heading="Replacement strings"></zn-slash-menu> </zn-textarea> <zn-textarea label="Internal note" rows="4" help-text="Type / — no hints" slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}, Jurisdiction={{JURISDICTION}}"> <zn-slash-menu slot="slash-menu" heading="Replacement strings" hide-hints></zn-slash-menu> </zn-textarea>
Recently Used
Set recent-key and the menu remembers what was chosen there, listing the most recent of those
items above the rest under their own heading. The key is where the menu is used — page-body,
ticket-reply — so each place keeps its own history in localStorage, and two fields
that should share one can share a key. max-recent caps the section (3 by default),
recent-heading names it, and clearRecent() forgets the lot. The section stands
aside as soon as there is a query, when the ranked matches are the better answer.
It reads as one more group, so it sits naturally above grouped items — the section’s own heading, then the sections the list already had. Insert a few from the first field below to see it fill:
<zn-textarea label="Privacy policy" rows="6" help-text="Type / and insert a few — they come back to the top"> <zn-slash-menu slot="slash-menu" recent-key="docs-grouped" style="--slash-menu-width: 340px"> <zn-slash-item group="Merchant" icon="tag@lu" label="Brand name" value="{{BRAND_NAME}}"></zn-slash-item> <zn-slash-item group="Merchant" icon="building@lu" label="Legal entity" value="{{LEGAL_ENTITY}}"></zn-slash-item> <zn-slash-item group="Customer" icon="user@lu" label="Customer name" value="{{CUSTOMER_NAME}}"></zn-slash-item> <zn-slash-item group="Customer" icon="mail@lu" label="Customer email" value="{{CUSTOMER_EMAIL}}"></zn-slash-item> <zn-slash-item group="Policy" icon="scale@lu" label="Jurisdiction" value="{{JURISDICTION}}"></zn-slash-item> </zn-slash-menu> </zn-textarea> <zn-button id="forget-recent" style="margin-top: 1rem">Forget them</zn-button> <script type="module"> import {clearRecentSlashItems} from '/dist/zn.min.js'; document.getElementById('forget-recent').addEventListener('click', () => { clearRecentSlashItems('docs-grouped'); clearRecentSlashItems('docs-terms'); }); </script>
Where the items below carry no heading of their own, a rule closes the section off instead. This field shares nothing with the one above — each key is its own history:
<zn-textarea label="Terms and conditions" rows="5" help-text="Type / and insert a few — the rule marks where they end" slash-recent-key="docs-terms" slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}, Jurisdiction={{JURISDICTION}}, Customer name={{CUSTOMER_NAME}}, Support email={{SUPPORT_EMAIL}}"></zn-textarea>
zn-input, zn-inline-edit, zn-translations and
zn-remarkd-editor take the same slash-recent-key. On a menu you slot in yourself,
or drive with SlashMenuController, set recent-key on the
zn-slash-menu directly.
Truncating Long Lists
max-items caps how many items are rendered; the rest are reported in a footer rather than
silently dropped. The list 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.160/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.160/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 name the list is announced by 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
|
|
hideKeys
hide-keys
|
Hides the insertion key (the item’s value) normally shown against each item. |
boolean
|
false
|
|
hideHints
hide-hints
|
Hides the pinned footer of keyboard hints. |
boolean
|
false
|
|
recentKey
recent-key
|
Remembers the items chosen here and lists the most recent of them first, under their own heading.
The key scopes the list to where the menu is used, so each place keeps its own history in
localStorage. Leave unset to offer no recently used section.
|
string
|
''
|
|
maxRecent
max-recent
|
The most recently used items to list. |
number
|
3
|
|
recentHeading
recent-heading
|
The heading shown above the recently used items. |
string
|
'Recently used'
|
|
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 |
|---|---|---|
clearRecent() |
Forgets the items remembered under recent-key. |
- |
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-border-radius |
The corner radius of the panel. | |
--slash-menu-item-border-radius |
The corner radius of the items and their icon chips. | |
--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. |
list |
The scrolling list of items. |
item |
An item in the list. |
icon |
The chip holding an item’s icon. |
group-heading |
A group heading between items. |
divider |
The rule closing the recently used section, when the items below it have no heading of their own. |
footer |
The truncation footer, shown when not every match fits. |
hints |
The pinned footer of keyboard hints. |
hint |
A single keyboard hint within the footer. |
hint-key |
The key shown against a hint. |
Learn more about customizing CSS parts.
Dependencies
This component automatically imports the following dependencies.
<zn-example><zn-icon>