← Back to Examples

🎛️ Action Buttons

Everything the action bar can do, each as ONE live picker you configure with control switches — built-in & static props, the dynamic callbacks (and their priority over statics), custom actions, icon buttons, layout, and a device-adaptive toolbar.

🌐 client — runs in the browser (element JS API) 🖥️ server — LiveView round-trip

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.

AB01 · Built-in & Static Properties

JS el.actionButtons · static per-button props: tooltip · cssClass · isVisible · isDisabled

The two built-in actions — select-all and clear-all — plus the static per-button properties: tooltip, cssClass, isVisible, and isDisabled. Toggle the switches to rebuild the actionButtons array live.

Built-in smart defaults:
With no static isDisabled (and no getIsDisabledCallback), Select All auto-disables once every option is selected and Clear All auto-disables while nothing is selected. Select all colors and watch Select All disable.
Selected:
[]
multiselect.actionButtons = [
  { action: 'select-all', text: 'Select All',
    tooltip: 'Select all available colors',   // static tooltip
    cssClass: 'custom-select-btn' },          // needs customStylesCallback (Shadow DOM)
  { action: 'clear-all', text: 'Clear All' },
  { action: 'custom', text: 'Hidden Button',   isVisible: false,  onClick: (ms) => {} },
  { action: 'custom', text: 'Disabled Button', isDisabled: true,  onClick: (ms) => {} },
];

AB02 · Dynamic Callbacks (& Priority)

Per-button callbacks on el.actionButtons: getIsVisibleCallback · getIsDisabledCallback · getTextCallback · getClassCallback · getTooltipCallback

Each button property has a dynamic twin that recomputes from the live component: getIsVisibleCallback, getIsDisabledCallback, getTextCallback, getClassCallback, and getTooltipCallback. Pick which one drives the buttons — or choose all (priority) to see every callback active at once, overriding the static text / isVisible / isDisabled that are also set.

callback:
Selected:
[]
Priority:
When both a static property and its callback are set, the callback wins: get*Callback > static > default. Try all (priority) — the buttons carry isVisible:false / isDisabled:true yet the callbacks keep them visible and enabled.

AB03 · Custom Actions with onClick

Per-button config on el.actionButtons: action: 'custom' + onClick: (ms) => …

Build your own buttons with action: 'custom' and an onClick(ms) handler that drives the component imperatively (setSelected, selectAll, getValue, …). Here: pick popular languages, invert the selection, or pick two at random — plus the built-in Clear All.

Selected:
[]
multiselect.actionButtons = [
  { action: 'custom', text: 'Select Popular',
    onClick: (ms) => ms.setSelected(['js', 'py', 'ts']) },
  { action: 'custom', text: 'Invert Selection',
    onClick: (ms) => {
      const all = ms.options.options.map(o => o[0]);
      const sel = ms.getValue();
      ms.setSelected(all.filter(v => !sel.includes(v)));
    } },
  { action: 'custom', text: 'Select Random',
    onClick: (ms) => {
      const shuffled = [...ms.options.options].sort(() => Math.random() - 0.5);
      ms.setSelected(shuffled.slice(0, 2).map(o => o[0]));
    } },
  { action: 'clear-all', text: 'Clear All' },
];

AB04 · Icon Buttons — Font Awesome vs Lucide (SVG)

Icon HTML in a button's text · Lucide SVG needs zero setup · Font Awesome needs the font at document level + rules via el.customStylesCallback

Button text accepts HTML, so icons work — but the two families need different setup in a Shadow DOM component. Font Awesome is font-based: the @font-face must live at document level (a link in the page head) and the .fa-* class rules must be injected into the shadow root via customStylesCallback. Lucide is inline SVG: it renders natively in Shadow DOM with zero setup and inherits the button color via stroke="currentColor". Flip the switch to compare.

icon library:
Selected:
[]
Prefer SVG in Shadow DOM:
Inline SVG needs no @font-face, no document-level link, and no CSS injection — the markup is self-contained and themes for free. Font icons work, but require both halves (font at document level + class rules in the shadow) or the glyphs render as empty □ boxes. Icon-only buttons should always carry a descriptive tooltip.

AB05 · Positioning, Rows & Alignment

actions_position="top | bottom" · per-button row · actions_align="stretch | left | center | right | space-between"

Place the actions block at the top (default) or bottom of the dropdown, flow buttons across multiple rows with the per-button row property, and set horizontal alignment with actions-align. Open the dropdown to see the bar move.

actions-position:
actions-align:
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).

AB06 · Device-Adaptive Actions

JS: import { observeViewport, classifyDevice } → swap el.actionButtons + el.actionsLayout per device/width

Twelve action buttons fit one row on a wide desktop, but there's no room on a phone, a tablet, or a narrowed window. The fix isn't a CSS trick: define your action sets and pick the right one for the space. The library re-exports the same device/viewport signal the component uses internally, so you react to the event that tells you the device and width and choose accordingly — here, granularly:

  • Desktop, wide (> 600px): all twelve buttons, one row (nowrap).
  • Desktop, narrowed (≤ 600px): all twelve, flowed onto two rows (wrap).
  • Tablet: all twelve wrapped — a single row would run beyond the edge.
  • Phone: the three essentials only, wrapped.
Selected:
[]
// Device + viewport signal, re-exported from the component package — one import,
// one dependency, the same signal the component reacts to internally.
import { observeViewport, classifyDevice } from '@keenmate/web-multiselect';

observeViewport((env) => {
  const device = classifyDevice(env);           // 'mobile' | 'tablet' | 'desktop'
  if (device === 'mobile') {                     // phone: essentials, wrapped
    el.actionButtons = mobileActions;  el.actionsLayout = 'wrap';
  } else if (device === 'tablet') {              // tablet: full set, wrapped
    el.actionButtons = desktopActions; el.actionsLayout = 'wrap';
  } else {                                        // desktop: two rows once ≤ 600px, else one
    el.actionButtons = desktopActions;
    el.actionsLayout = env.viewportWidth <= 600 ? 'wrap' : 'nowrap';
  }
});

AB07 · Summary

Static Properties

action: 'select-all' | 'clear-all' | 'custom'  // Required
text: string                                    // Required
row?: number                                    // Optional (default: 1) — which button row
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

Layout attributes

actions-position  // 'top' (default) | 'bottom'
actions-layout    // 'nowrap' (default) | 'wrap'
actions-align     // 'stretch' (default) | 'left' | 'center' | 'right' | 'space-between'

Priority Rules

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

🧩 Actions Layout — Wrap Mode (side-by-side) keen extra

actions_layout="wrap" · actions_layout="nowrap" (default)

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:
[]
// 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.
🔌 Server round-trip
0
The LiveView server saw 0 event(s) from the wrapper.
(pick an option anywhere on this page)