All menu plugins (Header Menu, Cell Menu, Context Menu, Grid Menu) support cross-framework compatible slot rendering for custom content injection in menu items. This is achieved through the slotRenderer callback at the item level combined with an optional defaultMenuItemRenderer at the menu level.
Note: This documentation covers how menu items are rendered (visual presentation). If you need to dynamically modify which commands appear in the menu (filtering, sorting, adding/removing items), see the
commandListBuildercallback documented in Grid Menu, Context Menu, or Header Menu.
When using commandListBuilder to add custom menu items with slotRenderer callbacks, cast the return value to the appropriate type to enable proper type parameters in callbacks:
contextMenu: {
commandListBuilder: (builtInItems) => {
return [
...builtInItems,
{
command: 'custom-action',
title: 'My Action',
slotRenderer: (cmdItem, args) => `<div>${cmdItem.title}</div>`,
}
] as Array<MenuCommandItem | GridMenuItem | 'divider'>;
}
}Alternatively, if you only have built-in items and dividers, you can use the simpler cast:
return [...] as Array<MenuCommandItem | 'divider'>;Each menu item can define a slotRenderer callback function that receives the item and args, and returns either an HTML string or an HTMLElement. This single API works uniformly across all menu plugins.
slotRenderer?: (cmdItem: MenuItem, args: MenuCallbackArgs, event?: Event) => string | HTMLElement- cmdItem - The menu cmdItem object containing command, title, iconCssClass, etc.
- args - The callback args providing access to grid, column, dataContext, and other context
- event - Optional DOM event passed during click handling (allows
stopPropagation())
const menuItem = {
command: 'custom-command',
title: 'Custom Action',
iconCssClass: 'mdi mdi-star',
// Return custom HTML string for the entire menu item
slotRenderer: () => `
<div class="custom-menu-item">
<i class="mdi mdi-star"></i>
<span>Custom Action</span>
<span class="badge">NEW</span>
</div>
`
};This approach is safer since it's CSP compliant with its use native HTML Elements. This could also be used to add event listeners or simply use the action callback.
// Create custom element with full DOM control
const menuItem = {
command: 'notifications',
title: 'Notifications',
// Return HTMLElement for more control and event listeners
slotRenderer: (cmdItem, args) => {
const container = document.createElement('div');
container.style.display = 'flex';
container.style.alignItems = 'center';
const icon = document.createElement('i');
icon.className = 'mdi mdi-bell';
icon.style.marginRight = '8px';
const text = document.createElement('span');
text.textContent = cmdItem.title;
const badge = document.createElement('span');
badge.className = 'badge';
badge.textContent = '5';
badge.style.marginLeft = 'auto';
container.appendChild(icon);
container.appendChild(text);
container.appendChild(badge);
return container;
}
};Set a defaultMenuItemRenderer at the menu option level to apply to all items (unless overridden by individual slotRenderer):
const menuOption = {
// Apply this renderer to all menu items (can be overridden per item)
defaultMenuItemRenderer: (cmdItem, args) => {
return `
<div style="display: flex; align-items: center; gap: 8px;">
${cmdItem.iconCssClass ? `<i class="${cmdItem.iconCssClass}" style="font-size: 18px;"></i>` : ''}
<span style="flex: 1;">${cmdItem.title}</span>
</div>
`;
},
commandItems: [
{
command: 'action-1',
title: 'Action One',
iconCssClass: 'mdi mdi-check',
// This item uses defaultMenuItemRenderer
},
{
command: 'custom',
title: 'Custom Item',
// This item overrides defaultMenuItemRenderer with its own slotRenderer
slotRenderer: () => `
<div style="color: #ff6b6b; font-weight: bold;">
Custom rendering overrides default
</div>
`
}
]
};The slotRenderer and defaultMenuItemRenderer work identically across all menu plugins:
const columnDef = {
id: 'name',
header: {
menu: {
defaultMenuItemRenderer: (cmdItem, args) => `<div>${cmdItem.title}</div>`,
commandItems: [
{
command: 'sort',
title: 'Sort',
slotRenderer: () => '<div>Custom sort</div>'
}
]
}
}
};const columnDef = {
id: 'action',
cellMenu: {
defaultMenuItemRenderer: (cmdItem, args) => `<div>${cmdItem.title}</div>`,
commandItems: [
{
command: 'edit',
title: 'Edit',
slotRenderer: (cmdItem, args) => `<div>Edit row ${args.dataContext.id}</div>`
}
]
}
};const gridOptions = {
enableContextMenu: true,
contextMenu: {
defaultMenuItemRenderer: (cmdItem, args) => `<div>${cmdItem.title}</div>`,
commandItems: [
{
command: 'export',
title: 'Export',
slotRenderer: () => '<div>📊 Export data</div>'
}
]
}
};const gridOptions = {
enableGridMenu: true,
gridMenu: {
defaultMenuItemRenderer: (cmdItem, args) => `<div>${cmdItem.title}</div>`,
commandItems: [
{
command: 'refresh',
title: 'Refresh',
slotRenderer: () => '<div>🔄 Refresh data</div>'
}
]
}
};{
command: 'copy',
title: 'Copy',
iconCssClass: 'mdi mdi-content-copy',
slotRenderer: () => `
<div style="display: flex; align-items: center; gap: 8px;">
<i class="mdi mdi-content-copy" style="font-size: 18px;"></i>
<span style="flex: 1;">Copy</span>
<kbd style="background: #eee; border: 1px solid #ccc; border-radius: 2px; padding: 2px 4px; font-size: 10px;">Ctrl+C</kbd>
</div>
`
}{
command: 'filter',
title: 'Filter',
iconCssClass: 'mdi mdi-filter',
slotRenderer: () => `
<div style="display: flex; align-items: center; gap: 8px;">
<i class="mdi mdi-filter" style="font-size: 18px;"></i>
<span style="flex: 1;">Filter</span>
<span style="width: 6px; height: 6px; border-radius: 50%; background: #44ff44; box-shadow: 0 0 4px #44ff44;"></span>
</div>
`
}{
command: 'edit-row',
title: 'Edit Row',
slotRenderer: (cmdItem, args) => `
<div style="display: flex; align-items: center; gap: 8px;">
<i class="mdi mdi-pencil" style="font-size: 18px;"></i>
<span>Edit Row #${args.dataContext?.id || 'N/A'}</span>
</div>
`
}{
command: 'toggle-setting',
title: 'Auto Refresh',
slotRenderer: (cmdItem, args, event) => {
const container = document.createElement('label');
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.gap = '8px';
container.style.marginRight = 'auto';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.addEventListener('change', (e) => {
// Prevent menu item click from firing when toggling checkbox
event?.stopPropagation?.();
console.log('Auto refresh:', checkbox.checked);
});
const label = document.createElement('span');
label.textContent = cmdItem.title;
container.appendChild(label);
container.appendChild(checkbox);
return container;
}
}{
command: 'export-excel',
title: 'Export as Excel',
slotRenderer: (cmdItem, args) => `
<div style="display: flex; align-items: center; gap: 8px;">
<i class="mdi mdi-file-excel-outline"></i>
<span style="flex: 1;">${cmdItem.title}</span>
<span style="background: #44ff44; color: #000; padding: 2px 4px; border-radius: 3px; font-size: 9px; font-weight: bold;">RECOMMENDED</span>
</div>
`
}{
command: 'advanced-export',
title: 'Advanced Export',
slotRenderer: (cmdItem, args) => {
const container = document.createElement('div');
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.gap = '8px';
const iconDiv = document.createElement('div');
iconDiv.style.width = '20px';
iconDiv.style.height = '20px';
iconDiv.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
iconDiv.style.borderRadius = '4px';
iconDiv.style.display = 'flex';
iconDiv.style.alignItems = 'center';
iconDiv.style.justifyContent = 'center';
iconDiv.style.color = 'white';
iconDiv.style.fontSize = '12px';
iconDiv.innerHTML = '📊';
const textSpan = document.createElement('span');
textSpan.textContent = cmdItem.title;
container.appendChild(iconDiv);
container.appendChild(textSpan);
return container;
}
}- HTML strings are inserted via
innerHTML- ensure content is sanitized if user-provided - HTMLElement objects are appended directly - safer for dynamic content and allows event listeners
- Cross-framework compatible - works in vanilla JS and any other frameworks using the same API
- Priority order - Item-level
slotRendereroverrides menu-leveldefaultMenuItemRenderer - Built-in command preservation - When overriding a built-in command (e.g.,
sort-asc,sort-desc,hide, etc.) with custom properties likeslotRendereroriconCssClass, if you don't provide anactioncallback, the library will automatically preserve and use the built-in action for that command. This means you can safely customize the appearance of built-in commands without losing their functionality. - Accessibility - Include proper ARIA attributes when creating custom elements
- Event handling - Call
event.stopPropagation()in interactive elements to prevent menu commands from firing and closing the menu - Default fallback - If neither
slotRenderernordefaultMenuItemRendereris provided, the default icon + text rendering is used - Performance - Avoid heavy DOM manipulation inside renderer callbacks (they may be called multiple times)
- Event parameter - The optional
eventparameter is passed during click handling and allows you to control menu behavior - All menus supported - This API works uniformly across Header Menu, Cell Menu, Context Menu, and Grid Menu
/* Example CSS for styled menu items */
.slick-menu-item {
padding: 4px 8px;
}
.slick-menu-item div {
display: flex;
align-items: center;
gap: 8px;
}
.slick-menu-item kbd {
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 3px;
padding: 2px 6px;
font-size: 11px;
font-family: monospace;
color: #666;
}
.slick-menu-item .badge {
background: #ff6b6b;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 9px;
font-weight: bold;
white-space: nowrap;
}
.slick-menu-item:hover {
background: #f5f5f5;
}
.slick-menu-item.slick-menu-item-disabled {
opacity: 0.5;
cursor: not-allowed;
}Before (Static HTML Title):
{
command: 'action',
title: 'Action ⭐', // Emoji embedded in title
iconCssClass: 'mdi mdi-star'
}After (Custom Rendering):
{
command: 'action',
title: 'Action',
slotRenderer: () => `
<div style="display: flex; align-items: center; gap: 8px;">
<i class="mdi mdi-star" style="font-size: 18px;"></i>
<span style="flex: 1;">Action</span>
<span style="font-size: 18px;">⭐</span>
</div>
`
}When creating custom renderers, handle potential errors gracefully:
{
command: 'safe-render',
title: 'Safe Render',
slotRenderer: (cmdItem, args) => {
try {
if (args?.dataContext?.status === 'error') {
return `<div style="color: red;">❌ Error loading</div>`;
}
return `<div>✓ Data loaded</div>`;
} catch (error) {
console.error('Render error:', error);
return '<div>Render error</div>';
}
}
}