Comprehensive guide to all action button configuration options
.actionButtons
property โ the callbacks (onClick, getTextCallback, โฆ) can't be
serialized as HTML attributes. Each demo below assigns its options and
config to the wrapper element by id after the custom element upgrades.
Simple select-all and clear-all buttons with default settings.
[]
multiselect.actionButtons = [
{
action: 'select-all',
text: 'Select All'
},
{
action: 'clear-all',
text: 'Clear All'
}
];
Using static properties: isVisible, isDisabled,
cssClass, and tooltip.
[]
multiselect.actionButtons = [
{
action: 'select-all',
text: 'Select All',
tooltip: 'Select all available colors',
cssClass: 'custom-select-btn',
isVisible: true,
isDisabled: false
},
{
action: 'clear-all',
text: 'Clear All',
tooltip: 'Remove all selections',
isVisible: true,
isDisabled: false
},
{
action: 'custom',
text: 'Hidden Button',
isVisible: false, // This button is hidden
onClick: (ms) => console.log('Clicked')
},
{
action: 'custom',
text: 'Disabled Button',
tooltip: 'This button is always disabled',
isDisabled: true, // This button is disabled
onClick: (ms) => console.log('Clicked')
}
];
Show/hide buttons based on current selection state using
getIsVisibleCallback.
[]
multiselect.actionButtons = [
{
action: 'select-all',
text: 'Select All',
tooltip: 'Select all numbers',
// Only show when not all items are selected
getIsVisibleCallback: (ms) => {
const total = ms.options.options?.length || 0;
const selected = ms.getSelected().length;
return selected < total;
}
},
{
action: 'clear-all',
text: 'Clear All',
tooltip: 'Clear selection',
// Only show when at least one item is selected
getIsVisibleCallback: (ms) => ms.getSelected().length > 0
}
];
Enable/disable buttons based on conditions using getIsDisabledCallback.
[]
multiselect.actionButtons = [
{
action: 'select-all',
text: 'Select All',
tooltip: 'Select all fruits',
// Disabled when all items are selected
getIsDisabledCallback: (ms) => {
const total = ms.options.options?.length || 0;
return ms.getSelected().length >= total;
}
},
{
action: 'clear-all',
text: 'Clear All',
tooltip: 'Clear selection',
// Disabled when nothing is selected
getIsDisabledCallback: (ms) => ms.getSelected().length === 0
},
{
action: 'custom',
text: 'Select First 3',
tooltip: 'Select first 3 items',
// Disabled when less than 3 items available
getIsDisabledCallback: (ms) => {
const total = ms.options.options?.length || 0;
return total < 3;
},
onClick: (ms) => {
const firstThree = ms.options.options.slice(0, 3).map(opt => opt[0]);
ms.setSelected(firstThree);
}
}
];
Change button text based on current state using getTextCallback.
[]
multiselect.actionButtons = [
{
action: 'select-all',
text: 'Select All', // Fallback text
// Show count in button text
getTextCallback: (ms) => {
const total = ms.options.options?.length || 0;
return `Select All (${total})`;
}
},
{
action: 'clear-all',
text: 'Clear All',
// Show selected count in button text
getTextCallback: (ms) => {
const count = ms.getSelected().length;
return count > 0 ? `Clear ${count} Selected` : 'Clear All';
}
},
{
action: 'custom',
text: 'Toggle',
// Toggle text based on state
getTextCallback: (ms) => {
const selected = ms.getSelected().length;
const total = ms.options.options?.length || 0;
return selected === total ? 'Deselect All' : 'Select All';
},
onClick: (ms) => {
const selected = ms.getSelected().length;
const total = ms.options.options?.length || 0;
if (selected === total) {
ms.setSelected([]);
} else {
ms.selectAll();
}
}
}
];
Apply CSS classes dynamically based on state using getClassCallback.
Custom styles are injected via customStylesCallback into the Shadow DOM.
[]
// Inject custom CSS into Shadow DOM
multiselect.customStylesCallback = () => `
.success-btn { background: #48bb78 !important; color: white !important; }
.warning-btn { background: #ed8936 !important; color: white !important; }
.danger-btn { background: #f56565 !important; color: white !important; }
.active-state { border: 2px solid #667eea !important; }
.inactive-state { opacity: 0.6; }
`;
multiselect.actionButtons = [
{
action: 'select-all',
text: 'Select All',
cssClass: 'default-class', // Fallback
// Return single class name as string
getClassCallback: (ms) => {
const selected = ms.getSelected().length;
return selected === 0 ? 'success-btn' : 'warning-btn';
}
},
{
action: 'clear-all',
text: 'Clear All',
// Return array of class names
getClassCallback: (ms) => {
const selected = ms.getSelected().length;
const classes = [];
if (selected > 0) {
classes.push('danger-btn', 'active-state');
} else {
classes.push('inactive-state');
}
return classes;
}
}
];
customStylesCallback
because the component uses Shadow DOM. Regular page styles won't affect
elements inside the shadow root. The callback can return a string or array of strings.
Show contextual information in tooltips using getTooltipCallback.
[]
multiselect.actionButtons = [
{
action: 'select-all',
text: 'Select All',
tooltip: 'Default tooltip', // Fallback
// Dynamic tooltip showing current state
getTooltipCallback: (ms) => {
const total = ms.options.options?.length || 0;
const selected = ms.getSelected().length;
const remaining = total - selected;
return `Select all ${total} items (${remaining} remaining)`;
}
},
{
action: 'clear-all',
text: 'Clear All',
// Show what will be cleared
getTooltipCallback: (ms) => {
const count = ms.getSelected().length;
return count > 0
? `Remove ${count} selected item${count !== 1 ? 's' : ''}`
: 'Nothing to clear';
}
},
{
action: 'custom',
text: 'Random Select',
// Contextual help in tooltip
getTooltipCallback: (ms) => {
const total = ms.options.options?.length || 0;
return `Randomly select 3 items from ${total} available`;
},
onClick: (ms) => {
const allOptions = ms.options.options || [];
const shuffled = [...allOptions].sort(() => Math.random() - 0.5);
const randomThree = shuffled.slice(0, 3).map(opt => opt[0]);
ms.setSelected(randomThree);
}
}
];
Create custom buttons with action: 'custom' and custom
onClick handlers.
[]
multiselect.actionButtons = [
{
action: 'custom',
text: 'Select Popular',
tooltip: 'Select JS, Python, and TypeScript',
onClick: (ms) => {
ms.setSelected(['js', 'py', 'ts']);
}
},
{
action: 'custom',
text: 'Invert Selection',
tooltip: 'Invert current selection',
onClick: (ms) => {
const allOptions = ms.options.options || [];
const allValues = allOptions.map(opt => opt[0]);
const selectedValues = ms.getValue();
const inverted = allValues.filter(v => !selectedValues.includes(v));
ms.setSelected(inverted);
}
},
{
action: 'custom',
text: 'Select Random',
tooltip: 'Select 2 random languages',
onClick: (ms) => {
const allOptions = ms.options.options || [];
const shuffled = [...allOptions].sort(() => Math.random() - 0.5);
const random = shuffled.slice(0, 2).map(opt => opt[0]);
ms.setSelected(random);
}
},
{
action: 'clear-all',
text: 'Clear All'
}
];
Demonstrating multiple callbacks working together and callback priority over static properties.
[]
multiselect.actionButtons = [
{
action: 'select-all',
// Static properties (will be OVERRIDDEN by callbacks)
text: 'Static Text',
tooltip: 'Static Tooltip',
cssClass: 'static-class',
isVisible: false, // Would hide, but callback overrides
isDisabled: true, // Would disable, but callback overrides
// Dynamic callbacks (TAKE PRIORITY)
getTextCallback: (ms) => {
const total = ms.options.options?.length || 0;
const selected = ms.getSelected().length;
return `Select All (${selected}/${total})`;
},
getTooltipCallback: (ms) => {
const selected = ms.getSelected().length;
return selected >= 5
? 'Maximum 5 items allowed'
: 'Click to select all items';
},
getClassCallback: (ms) => {
const selected = ms.getSelected().length;
return selected >= 5 ? 'danger-btn' : 'success-btn';
},
getIsVisibleCallback: (ms) => {
const total = ms.options.options?.length || 0;
const selected = ms.getSelected().length;
return selected < total; // Always visible when not all selected
},
getIsDisabledCallback: (ms) => {
return ms.getSelected().length >= 5; // Disabled at max
}
},
{
action: 'clear-all',
text: 'Clear',
getTextCallback: (ms) => {
const count = ms.getSelected().length;
return `Clear (${count})`;
},
getClassCallback: (ms) => {
return ms.getSelected().length > 0 ? ['danger-btn', 'active-state'] : 'inactive-state';
},
getIsVisibleCallback: (ms) => ms.getSelected().length > 0,
getIsDisabledCallback: (ms) => ms.getSelected().length === 0
}
];
isVisible: false and isDisabled: true are set,
the callbacks take priority and determine the actual state.
When you have many action buttons, use actions-layout="wrap"
to allow buttons to wrap to multiple rows instead of being squeezed into a
single row. Compare the behavior with 12 buttons below.
[]
[]
// Example with 12 action buttons
multiselect.actionButtons = [
{ action: 'select-all', text: 'Select All Items' },
{ action: 'clear-all', text: 'Clear Selection' },
{ action: 'custom', text: 'First 3 Items', onClick: ... },
{ action: 'custom', text: 'Last 3 Items', onClick: ... },
{ action: 'custom', text: 'Even Positions', onClick: ... },
{ action: 'custom', text: 'Odd Positions', onClick: ... },
{ action: 'custom', text: 'Random Selection', onClick: ... },
{ action: 'custom', text: 'First Half', onClick: ... },
{ action: 'custom', text: 'Second Half', onClick: ... },
{ action: 'custom', text: 'Invert', onClick: ... },
{ action: 'custom', text: 'Every Third', onClick: ... },
{ action: 'custom', text: 'Shuffle All', onClick: ... }
];
// Default - buttons squeezed in single row
<web-multiselect actions-layout="nowrap"></web-multiselect>
// Wrap mode - buttons wrap to multiple rows
<web-multiselect actions-layout="wrap"></web-multiselect>
Action buttons accept HTML, so Font Awesome icons work โ but Font Awesome is
font-based, which needs two things in a
Shadow DOM component: (1) the @font-face must be
registered at the document level (a normal <link>
in the page <head>) โ a shadow-scoped @font-face
is ignored by browsers, so the glyphs would render as empty boxes; and
(2) the icon class rules
(.fas, .fa-*::before) must be injected into
the Shadow DOM via customStylesCallback, because page CSS can't
cross the shadow boundary. Prefer SVG icons (ยง12) when you want zero setup.
[]
// STEP 0: Register the FONT at document level (page <head>), once:
// <link rel="stylesheet" href="https://cdnjs.cloudflare.com/.../font-awesome/6.5.1/css/all.min.css">
// A shadow-scoped @font-face is ignored, so this is what makes the glyphs render.
// STEP 1: Inject the icon CLASS RULES into the Shadow DOM
multiselect.customStylesCallback = () => `
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css');
/* Optional: Style the icons */
.ms__action-btn i {
margin-right: 0.35rem;
}
`;
// STEP 2: Use Font Awesome icons in buttons
multiselect.actionButtons = [
{
action: 'select-all',
text: '<i class="fas fa-check-double"></i> Select All',
tooltip: 'Select all available items'
},
{
action: 'clear-all',
text: '<i class="fas fa-times"></i> Clear',
tooltip: 'Clear selection'
},
{
action: 'custom',
text: '<i class="fas fa-random"></i> Random',
tooltip: 'Select 3 random items',
onClick: (ms) => {
const shuffled = [...options].sort(() => Math.random() - 0.5);
ms.setSelected(shuffled.slice(0, 3).map(o => o[0]));
}
},
// Icon-only button
{
action: 'custom',
text: '<i class="fas fa-sync-alt"></i>',
tooltip: 'Invert Selection', // Essential for accessibility
onClick: (ms) => { /* ... */ }
},
// Dynamic icon with getTextCallback
{
action: 'custom',
text: 'Toggle', // Fallback
getTextCallback: (ms) => {
const count = ms.getSelected().length;
const total = ms.options.options?.length || 0;
const icon = count === total
? '<i class="fas fa-toggle-on"></i>'
: '<i class="fas fa-toggle-off"></i>';
return `${icon} ${count}/${total}`;
},
onClick: (ms) => { /* ... */ }
}
];
<link> in the page
<head>. The @font-face must live in the
document โ a browser will not apply a @font-face
declared inside a shadow root, so loading FA only via
customStylesCallback renders the glyphs as empty
โก boxes.
.fas / .fa-*::before rules must be injected
into the Shadow DOM with customStylesCallback, because page
CSS doesn't cross the shadow boundary. The @import above does
this (or inject just the few ::before { content } rules you
use to avoid the async fetch).
@font-face and no CSS injection at all.
text property and getTextCallback both
accept HTML strings.
Unlike font icons, inline SVG icons render natively inside Shadow DOM
โ no @font-face, no <head> link, no
customStylesCallback. You just put the SVG markup in the button
text. These are Lucide icons; they use
stroke="currentColor", so they automatically match the button's
text color (including dark mode).
[]
// No setup needed โ SVG just works in Shadow DOM.
// Lucide SVGs use stroke="currentColor" so they inherit the button color.
const check = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="vertical-align:-3px"><path d="M18 6 7 17l-5-5"/><path d="m22 10-7.5 7.5L13 16"/></svg>`;
multiselect.actionButtons = [
{ action: 'select-all', text: `${check} Select All`, tooltip: 'Select all' },
{ action: 'clear-all', text: `${xIcon} Clear`, tooltip: 'Clear selection' },
// ...custom buttons with shuffle / refresh / toggle SVGs
];
@import
race โ the markup is self-contained.
stroke="currentColor" (Lucide) / fill="currentColor"
means the icon follows the button's text color, so dark mode and custom
--ms-* colors just work.
width/height on the <svg>
(here 16) and a small vertical-align to sit it on
the text baseline.
text/getTextCallback render raw HTML โ only
inline SVG you control, never untrusted strings.
Place the actions block at the top (default) or bottom of the
dropdown, arrange buttons across multiple rows with the per-button row
property, and control horizontal alignment with actions_align.
actions_position="top" โ 2 rowsRow 1 is the topmost line, row 2 below it.
actions_position="bottom" โ 2 rowsRow 1 is the bottommost line, row 2 above it.
actions_align)<web-multiselect actions-position="bottom" actions-align="right"></web-multiselect>
multiselect.actionButtons = [
{ action: 'select-all', text: 'Select All', row: 1 },
{ action: 'clear-all', text: 'Clear All', row: 1 },
{ action: 'custom', text: 'First 3', row: 2, onClick: ... },
{ action: 'custom', text: 'Invert', row: 2, onClick: ... },
];
top renders row 1 first (top), and bottom renders row 1
last (bottom).
action: 'select-all' | 'clear-all' | 'custom' // Required
text: string // Required
tooltip?: string // Optional
cssClass?: string // Optional
isVisible?: boolean // Optional (default: true)
isDisabled?: boolean // Optional (default: false)
onClick?: (multiselect) => void | Promise<void> // Required for 'custom' action
getIsVisibleCallback?: (multiselect) => boolean // Dynamic visibility
getIsDisabledCallback?: (multiselect) => boolean // Dynamic disabled state
getTextCallback?: (multiselect) => string // Dynamic text
getClassCallback?: (multiselect) => string | string[] // Dynamic CSS classes
getTooltipCallback?: (multiselect) => string // Dynamic tooltip
getIsVisibleCallback > isVisible > default truegetIsDisabledCallback > isDisabled > default falsegetTextCallback > textgetClassCallback > cssClassgetTooltipCallback > tooltip