โ† Back to Examples

๐ŸŽ›๏ธ Action Buttons

Comprehensive guide to all action button configuration options

Why this page uses inline scripts
Action buttons are configured via the JS-side .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.

1. Basic Built-in Actions

Simple select-all and clear-all buttons with default settings.

Selected:
[]
multiselect.actionButtons = [
  {
    action: 'select-all',
    text: 'Select All'
  },
  {
    action: 'clear-all',
    text: 'Clear All'
  }
];

2. Static Properties

Using static properties: isVisible, isDisabled, cssClass, and tooltip.

Selected:
[]
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')
  }
];

3. Dynamic Visibility (getIsVisibleCallback)

Show/hide buttons based on current selection state using getIsVisibleCallback.

Selected:
[]
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
  }
];
Try it:
Select some items to see "Clear All" appear. Select all items to see "Select All" disappear.

4. Dynamic Disabled State (getIsDisabledCallback)

Enable/disable buttons based on conditions using getIsDisabledCallback.

Selected:
[]
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);
    }
  }
];

5. Dynamic Text (getTextCallback)

Change button text based on current state using getTextCallback.

Selected:
[]
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();
      }
    }
  }
];
Try it:
Watch the button text change as you select/deselect items.

6. Dynamic CSS Classes (getClassCallback)

Apply CSS classes dynamically based on state using getClassCallback. Custom styles are injected via customStylesCallback into the Shadow DOM.

Selected:
[]
// 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;
    }
  }
];
Important:
Custom CSS classes must be injected via 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.

7. Dynamic Tooltip (getTooltipCallback)

Show contextual information in tooltips using getTooltipCallback.

Selected:
[]
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);
    }
  }
];
Try it:
Hover over the buttons to see dynamic tooltips that change based on the current state.

8. Custom Actions with onClick

Create custom buttons with action: 'custom' and custom onClick handlers.

Selected:
[]
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'
  }
];

9. Combined Features & Callback Priority

Demonstrating multiple callbacks working together and callback priority over static properties.

Selected:
[]
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
  }
];
Callback Priority:
Notice how the callbacks override the static properties. Even though isVisible: false and isDisabled: true are set, the callbacks take priority and determine the actual state.

10. Actions Layout - Wrap Mode

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.

Default (nowrap)

Selected:
[]

Wrap Mode

Selected:
[]
// 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>
Visual Difference:
With 12 buttons, the difference is clear: nowrap forces all buttons into one row (they become very narrow), while wrap allows them to flow naturally across multiple rows with comfortable sizing.

11. Font Awesome Icons in Buttons

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.

Selected:
[]
// 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) => { /* ... */ }
  }
];
โš ๏ธ Shadow DOM + font icons โ€” you need both halves:
  • Font at document level (required): Keep the Font Awesome <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.
  • Class rules in the shadow (required): The .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).
  • Prefer SVG icons (zero setup): Inline SVG (e.g. Lucide โ€” see ยง12) renders natively in Shadow DOM with no @font-face and no CSS injection at all.
  • HTML is supported: The text property and getTextCallback both accept HTML strings.
  • Accessibility: Icon-only buttons should always include descriptive tooltips for screen readers.
  • Any font-icon library works the same way: Material Icons, Bootstrap Icons, etc. โ€” font at document level, class rules in the shadow.

12. Lucide (SVG) Icons in Buttons

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).

Selected:
[]
// 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
];
โœ… Why SVG is the easy path in Shadow DOM:
  • Zero setup: No font registration, no CSS injection, no async @import race โ€” the markup is self-contained.
  • Themes for free: stroke="currentColor" (Lucide) / fill="currentColor" means the icon follows the button's text color, so dark mode and custom --ms-* colors just work.
  • Sizing: Set width/height on the <svg> (here 16) and a small vertical-align to sit it on the text baseline.
  • Security: text/getTextCallback render raw HTML โ€” only inline SVG you control, never untrusted strings.

13. Positioning, Rows & Alignment

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 rows

Row 1 is the topmost line, row 2 below it.

actions_position="bottom" โ€” 2 rows

Row 1 is the bottommost line, row 2 above it.

Alignment (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: ... },
];
Row ordering:
Row 1 always sits at the panel's outer edge; higher rows stack inward toward the options list. So top renders row 1 first (top), and bottom renders row 1 last (bottom).

๐Ÿ“‹ Summary

Static Properties

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)

Dynamic Callbacks

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

Priority Rules

When both static and callback are defined:
  • getIsVisibleCallback > isVisible > default true
  • getIsDisabledCallback > isDisabled > default false
  • getTextCallback > text
  • getClassCallback > cssClass
  • getTooltipCallback > tooltip