@keenmate/web-multiselect 1.0.0-rc11 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,8 +12,8 @@ A lightweight, accessible multiselect web component with typeahead search, RTL l
12
12
  - 🔍 **Flexible Search Modes** - Filter (hide non-matches) or navigate (jump to matches, keep all visible)
13
13
  - ⌨️ **Keyboard Navigation** - Full keyboard support (arrows, Enter, Esc, Tab)
14
14
  - 🎨 **Rich Content** - Icons, subtitles, and multiline text support
15
- - 📊 **Multiple Display Modes** - Pills, count, compact, partial, or none (minimal UI)
16
- - 💬 **Pill Tooltips** - Customizable tooltips on selected items with placement control
15
+ - 📊 **Multiple Display Modes** - Badges, count, compact, partial, or none (minimal UI)
16
+ - 💬 **Badge Tooltips** - Customizable tooltips on selected items with placement control
17
17
  - 🎯 **Single & Multi-Select** - Switch between single and multiple selection modes
18
18
  - 🔄 **Async Data Loading** - On-demand data fetching support
19
19
  - 📦 **Grouped Options** - Organize options into collapsible groups
@@ -111,27 +111,26 @@ multiselect.setSelected(['js', 'ts']);
111
111
  | `search-placeholder` | `string` | `'Search...'` | Placeholder text for search input |
112
112
  | `search-hint` | `string` | - | Hint text shown above input when focused |
113
113
  | `allow-groups` | `boolean` | `true` | Enable option grouping |
114
- | `allow-select-all` | `boolean` | `true` | Show "Select All" button |
115
- | `allow-clear-all` | `boolean` | `true` | Show "Clear All" button |
116
114
  | `show-checkboxes` | `boolean` | `true` | Show checkboxes next to options |
117
115
  | `close-on-select` | `boolean` | `false` | Close dropdown after selecting |
118
116
  | `dropdown-min-width` | `string` | - | Min width for dropdown (e.g., '20rem') |
119
- | `pills-display-mode` | `'pills' \| 'count' \| 'compact' \| 'partial' \| 'none'` | `'pills'` | How to display selected items. `compact`: first item + count. `none`: no display |
120
- | `pills-threshold` | `number` | - | Auto-switch mode when exceeded (see pills-threshold-mode) |
121
- | `pills-threshold-mode` | `'count' \| 'partial'` | `'count'` | Mode after threshold: 'count' shows badge, 'partial' shows limited pills + more badge |
122
- | `pills-max-visible` | `number` | `3` | Max pills shown in partial mode |
123
- | `pills-position` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'bottom'` | Position of pills container |
124
- | `show-count-badge` | `boolean` | `false` | Show [3] badge next to toggle icon |
125
- | `enable-pill-tooltips` | `boolean` | `false` | Enable tooltips on selected pills |
126
- | `pill-tooltip-placement` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | Tooltip placement relative to pill |
127
- | `pill-tooltip-delay` | `number` | `300` | Delay in ms before showing tooltip |
128
- | `pill-tooltip-offset` | `number` | `8` | Distance in pixels between pill and tooltip |
117
+ | `badges-display-mode` | `'pills' \| 'count' \| 'compact' \| 'partial' \| 'none'` | `'pills'` | How to display selected items. `compact`: first item + count. `none`: no display |
118
+ | `badges-threshold` | `number` | - | Auto-switch mode when exceeded (see badges-threshold-mode) |
119
+ | `badges-threshold-mode` | `'count' \| 'partial'` | `'count'` | Mode after threshold: 'count' shows badge, 'partial' shows limited badges + more badge |
120
+ | `badges-max-visible` | `number` | `3` | Max badges shown in partial mode |
121
+ | `badges-position` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'bottom'` | Position of badges container |
122
+ | `show-counter` | `boolean` | `false` | Show [3] badge next to toggle icon |
123
+ | `enable-badge-tooltips` | `boolean` | `false` | Enable tooltips on selected badges |
124
+ | `badge-tooltip-placement` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | Tooltip placement relative to badge |
125
+ | `badge-tooltip-delay` | `number` | `300` | Delay in ms before showing tooltip |
126
+ | `badge-tooltip-offset` | `number` | `8` | Distance in pixels between badge and tooltip |
129
127
  | `max-height` | `string` | `'20rem'` | Maximum height of dropdown |
130
128
  | `empty-message` | `string` | `'No results found'` | Message when no options found |
131
129
  | `loading-message` | `string` | `'Loading...'` | Message while loading async data |
132
130
  | `min-search-length` | `number` | `0` | Minimum search length for async |
133
131
  | `keep-options-on-search` | `boolean` | `true` | Keep initial options visible when searchCallback is active (hybrid search) |
134
- | `sticky-actions` | `boolean` | `true` | Keep Select All/Clear All buttons fixed at top while scrolling |
132
+ | `sticky-actions` | `boolean` | `true` | Keep action buttons fixed at top while scrolling |
133
+ | `actions-layout` | `'nowrap' \| 'wrap'` | `'nowrap'` | Layout mode for action buttons: 'nowrap' (single row) or 'wrap' (multi-row) |
135
134
  | `lock-placement` | `boolean` | `true` | Lock dropdown placement after first open to prevent flipping |
136
135
  | `enable-search` | `boolean` | `true` | Enable/disable search functionality |
137
136
  | `search-input-mode` | `'normal' \| 'readonly' \| 'hidden'` | `'normal'` | Search input display mode |
@@ -191,19 +190,53 @@ multiselect.onChange = (selectedOptions) => {
191
190
  console.log('Changed:', selectedOptions);
192
191
  };
193
192
 
194
- // Pill display customization (show different text in pills vs dropdown)
195
- multiselect.getPillDisplayCallback = (item) => {
196
- // Show shorter text in pills (e.g., just name instead of "name (email)")
193
+ // Badge display customization (show different text in badges vs dropdown)
194
+ multiselect.getBadgeDisplayCallback = (item) => {
195
+ // Show shorter text in badges (e.g., just name instead of "name (email)")
197
196
  return item.name; // Dropdown might show "John Doe (john@example.com)"
198
197
  };
199
198
 
200
- // Pill tooltip customization
201
- multiselect.getPillTooltipCallback = (item) => {
199
+ // Badge tooltip customization
200
+ multiselect.getBadgeTooltipCallback = (item) => {
202
201
  return `${item.label} - ${item.subtitle}`;
203
202
  };
204
203
 
205
- // Count pill i18n/pluralization
206
- multiselect.getCountPillCallback = (count, moreCount) => {
204
+ // Action buttons (Select All, Clear All, custom actions)
205
+ multiselect.actionButtons = [
206
+ {
207
+ action: 'select-all',
208
+ text: 'Select All',
209
+ tooltip: 'Select all items',
210
+ cssClass: 'my-custom-class',
211
+ isVisibleCallback: (multiselect) => multiselect.getSelected().length < 5 // Hide if 5+ selected
212
+ },
213
+ {
214
+ action: 'clear-all',
215
+ text: 'Clear All',
216
+ tooltip: 'Clear selection',
217
+ isVisible: true, // Static visibility
218
+ isDisabled: false // Static disabled state
219
+ },
220
+ {
221
+ action: 'custom',
222
+ text: 'Invert',
223
+ tooltip: 'Invert selection',
224
+ onClick: (multiselect) => {
225
+ // Custom action - invert selection
226
+ const allValues = multiselect.options.map(opt => opt.value);
227
+ const selectedValues = multiselect.getValue();
228
+ const inverted = allValues.filter(v => !selectedValues.includes(v));
229
+ multiselect.setSelected(inverted);
230
+ },
231
+ // Dynamic callbacks (take priority over static properties)
232
+ isDisabledCallback: (multiselect) => multiselect.getSelected().length === 0,
233
+ getTextCallback: (multiselect) => multiselect.getSelected().length > 0 ? 'Invert' : 'Select Items First',
234
+ getClassCallback: (multiselect) => multiselect.getSelected().length > 0 ? 'active' : 'inactive'
235
+ }
236
+ ];
237
+
238
+ // Counter i18n/pluralization
239
+ multiselect.getCounterCallback = (count, moreCount) => {
207
240
  if (moreCount !== undefined) {
208
241
  return `+${moreCount} more`; // Partial mode badge
209
242
  }
@@ -227,6 +260,24 @@ multiselect.getSubtitleCallback = (item) => `${item.price} - ${item.stock} in st
227
260
  multiselect.getGroupCallback = (item) => item.category;
228
261
  multiselect.getDisabledCallback = (item) => item.stock === 0;
229
262
 
263
+ // Custom rendering - Full HTML control
264
+ multiselect.renderOptionContentCallback = (item, context) => {
265
+ // Customize option content (HTML string or HTMLElement)
266
+ return `<strong>${item.name}</strong> <span class="badge">${item.status}</span>`;
267
+ };
268
+
269
+ multiselect.renderBadgeContentCallback = (item, context) => {
270
+ // Customize badge content (HTML string or HTMLElement)
271
+ return context.isInPopover
272
+ ? `${item.icon} ${item.name} - ${item.description}`
273
+ : `${item.icon} ${item.name}`;
274
+ };
275
+
276
+ multiselect.renderSelectedContentCallback = (item) => {
277
+ // Customize selected item text in single-select mode (plain text only)
278
+ return item.firstName; // Show just first name when closed
279
+ };
280
+
230
281
  // Form integration
231
282
  multiselect.name = 'selected_items';
232
283
  multiselect.valueFormat = 'json'; // 'json' | 'csv' | 'array'
@@ -560,86 +611,91 @@ Choose between two search behaviors:
560
611
  Perfect for different use cases and space constraints:
561
612
 
562
613
  ```html
563
- <!-- Pills mode (default) - Show all selections as removable pills -->
564
- <web-multiselect pills-display-mode="pills"></web-multiselect>
614
+ <!-- Badges mode (default) - Show all selections as removable badges -->
615
+ <web-multiselect badges-display-mode="pills"></web-multiselect>
565
616
 
566
617
  <!-- Count mode - Show "X selected" text with clear button -->
567
- <web-multiselect pills-display-mode="count" show-count-badge="true"></web-multiselect>
618
+ <web-multiselect badges-display-mode="count" show-counter="true"></web-multiselect>
568
619
 
569
- <!-- Compact mode - Show first item + count in a single removable pill -->
570
- <web-multiselect pills-display-mode="compact"></web-multiselect>
620
+ <!-- Compact mode - Show first item + count in a single removable badge -->
621
+ <web-multiselect badges-display-mode="compact"></web-multiselect>
571
622
  <!-- Example output: [JavaScript (+2 more) | x] -->
572
623
 
573
- <!-- None mode - No display in pills area (minimal UI) -->
574
- <web-multiselect pills-display-mode="none" show-count-badge="true"></web-multiselect>
624
+ <!-- None mode - No display in badges area (minimal UI) -->
625
+ <web-multiselect badges-display-mode="none" show-counter="true"></web-multiselect>
575
626
  <!-- Only shows [X] badge next to toggle icon -->
576
627
 
577
- <!-- Auto-switch from pills to count at threshold -->
628
+ <!-- Auto-switch from badges to count at threshold -->
578
629
  <web-multiselect
579
- pills-threshold="3"
580
- pills-threshold-mode="count"
581
- show-count-badge="true">
630
+ badges-threshold="3"
631
+ badges-threshold-mode="count"
632
+ show-counter="true">
582
633
  </web-multiselect>
583
634
 
584
- <!-- Partial mode - Show limited pills + "+X more" badge -->
635
+ <!-- Partial mode - Show limited badges + "+X more" badge -->
585
636
  <web-multiselect
586
- pills-threshold="5"
587
- pills-threshold-mode="partial"
588
- pills-max-visible="3">
637
+ badges-threshold="5"
638
+ badges-threshold-mode="partial"
639
+ badges-max-visible="3">
589
640
  </web-multiselect>
590
641
  ```
591
642
 
592
643
  **Display Mode Behavior:**
593
- - **`pills`**: Individual removable pills for each selected item. Calls `getPillDisplayCallback` for each item.
594
- - **`count`**: Shows "X selected" text with clear button. Calls `getCountPillCallback(count)`.
595
- - **`compact`**: Shows first item + count in single pill (e.g., "JavaScript (+2 more)"). Calls `getPillDisplayCallback(firstItem)` and `getCountPillCallback(count, remainingCount)`.
596
- - **`partial`**: Shows first N pills + "+X more" badge. Calls `getPillDisplayCallback` for visible items and `getCountPillCallback(count, remainingCount)` for badge.
597
- - **`none`**: No display in pills area. No callbacks invoked. Use with `show-count-badge="true"` for minimal UI.
644
+ - **`pills`**: Individual removable badges for each selected item. Calls `getBadgeDisplayCallback` for each item.
645
+ - **`count`**: Shows "X selected" text with clear button. Calls `getCounterCallback(count)`.
646
+ - **`compact`**: Shows first item + count in single badge (e.g., "JavaScript (+2 more)"). Calls `getBadgeDisplayCallback(firstItem)` and `getCounterCallback(count, remainingCount)`.
647
+ - **`partial`**: Shows first N badges + "+X more" badge. Calls `getBadgeDisplayCallback` for visible items and `getCounterCallback(count, remainingCount)` for badge.
648
+ - **`none`**: No display in badges area. No callbacks invoked. Use with `show-counter="true"` for minimal UI.
649
+
650
+ **Badge Styling:**
651
+ - **Data badges** (selected items like "JavaScript", "Python"): Blue styling by default
652
+ - **BadgeCounters** ("+3 more", "5 selected", compact mode display): Gray styling to distinguish from data
653
+ - Both can be customized via CSS variables (see `--ml-badge-*` and `--ml-badge-counter-*`)
598
654
 
599
- **Count Badge (`show-count-badge="true"`)**: Independent feature showing `[X]` next to toggle icon. Works with all display modes. Not affected by callbacks.
655
+ **Counter (`show-counter="true"`)**: Independent feature showing `[X]` next to toggle icon. Works with all display modes. Not affected by callbacks.
600
656
 
601
- ### Pills Positioning
657
+ ### Badge Positioning
602
658
 
603
659
  Control where selected item badges appear relative to the input:
604
660
 
605
661
  ```html
606
- <!-- Pills below input (default) -->
607
- <web-multiselect pills-position="bottom"></web-multiselect>
662
+ <!-- Badges below input (default) -->
663
+ <web-multiselect badges-position="bottom"></web-multiselect>
608
664
 
609
- <!-- Pills above input -->
610
- <web-multiselect pills-position="top"></web-multiselect>
665
+ <!-- Badges above input -->
666
+ <web-multiselect badges-position="top"></web-multiselect>
611
667
 
612
- <!-- Pills to the left of input -->
613
- <web-multiselect pills-position="left"></web-multiselect>
668
+ <!-- Badges to the left of input -->
669
+ <web-multiselect badges-position="left"></web-multiselect>
614
670
 
615
- <!-- Pills to the right of input -->
616
- <web-multiselect pills-position="right"></web-multiselect>
671
+ <!-- Badges to the right of input -->
672
+ <web-multiselect badges-position="right"></web-multiselect>
617
673
  ```
618
674
 
619
- **Note:** In RTL mode, left/right positions are automatically mirrored - `pills-position="left"` will appear on the physical right side in RTL languages.
675
+ **Note:** In RTL mode, left/right positions are automatically mirrored - `badges-position="left"` will appear on the physical right side in RTL languages.
620
676
 
621
- ### Pill Tooltips
677
+ ### Badge Tooltips
622
678
 
623
- Enable tooltips on selected item pills with customizable placement and delay:
679
+ Enable tooltips on selected item badges with customizable placement and delay:
624
680
 
625
681
  ```html
626
682
  <!-- Basic tooltips -->
627
683
  <web-multiselect
628
- enable-pill-tooltips="true"
629
- pill-tooltip-placement="top">
684
+ enable-badge-tooltips="true"
685
+ badge-tooltip-placement="top">
630
686
  </web-multiselect>
631
687
 
632
688
  <!-- Fast tooltips with custom delay -->
633
689
  <web-multiselect
634
- enable-pill-tooltips="true"
635
- pill-tooltip-delay="100">
690
+ enable-badge-tooltips="true"
691
+ badge-tooltip-delay="100">
636
692
  </web-multiselect>
637
693
 
638
694
  <script type="module">
639
695
  const select = document.querySelector('web-multiselect');
640
696
 
641
697
  // Custom tooltip content
642
- select.getPillTooltipCallback = (item) => {
698
+ select.getBadgeTooltipCallback = (item) => {
643
699
  return `${item.label} - ${item.subtitle}`;
644
700
  };
645
701
  </script>
@@ -647,21 +703,21 @@ Enable tooltips on selected item pills with customizable placement and delay:
647
703
 
648
704
  ### Internationalization (i18n)
649
705
 
650
- Customize count pill text for proper pluralization and localization:
706
+ Customize counter text for proper pluralization and localization:
651
707
 
652
708
  ```html
653
709
  <web-multiselect
654
710
  id="i18n-select"
655
- pills-threshold="5"
656
- pills-threshold-mode="partial"
657
- pills-max-visible="3">
711
+ badges-threshold="5"
712
+ badges-threshold-mode="partial"
713
+ badges-max-visible="3">
658
714
  </web-multiselect>
659
715
 
660
716
  <script type="module">
661
717
  const select = document.getElementById('i18n-select');
662
718
 
663
719
  // Spanish pluralization example
664
- select.getCountPillCallback = (count, moreCount) => {
720
+ select.getCounterCallback = (count, moreCount) => {
665
721
  if (moreCount !== undefined) {
666
722
  // Partial mode: "+X more" badge
667
723
  return moreCount === 1 ? '+1 más' : `+${moreCount} más`;
@@ -693,12 +749,419 @@ Full RTL support for Arabic, Hebrew, Persian, Urdu, and other right-to-left lang
693
749
 
694
750
  **RTL Features:**
695
751
  - ✅ **Auto-detection** - Detects `dir="rtl"` on component or any ancestor element
696
- - ✅ **Complete UI mirroring** - Toggle icon, text alignment, pills, dropdown, badges
697
- - ✅ **Logical positioning** - `pills-position="left"` becomes physically right in RTL
698
- - ✅ **Pills remove buttons** - Flip to left side in RTL mode
752
+ - ✅ **Complete UI mirroring** - Toggle icon, text alignment, badges, dropdown, badges
753
+ - ✅ **Logical positioning** - `badges-position="left"` becomes physically right in RTL
754
+ - ✅ **Badge remove buttons** - Flip to left side in RTL mode
699
755
  - ✅ **Text direction** - All text content properly right-aligned
700
756
  - ✅ **No configuration needed** - Just set `dir="rtl"` attribute
701
757
 
758
+ ### Custom Rendering
759
+
760
+ The component provides powerful custom rendering callbacks that allow you to fully customize how options, badges, and selected items are displayed while maintaining the component's structure and functionality.
761
+
762
+ #### Overview
763
+
764
+ Three rendering callbacks are available:
765
+ - **`renderOptionContentCallback`** - Customize dropdown option content
766
+ - **`renderBadgeContentCallback`** - Customize badge (selected item) content
767
+ - **`renderSelectedContentCallback`** - Customize selected value text (single-select mode)
768
+
769
+ All callbacks can return either **HTML strings** or **HTMLElement** objects (except `renderSelectedContentCallback` which returns plain text).
770
+
771
+ #### Custom Option Rendering
772
+
773
+ Customize how options appear in the dropdown:
774
+
775
+ ```html
776
+ <web-multiselect id="custom-options"></web-multiselect>
777
+
778
+ <script type="module">
779
+ import '@keenmate/web-multiselect';
780
+
781
+ const select = document.getElementById('custom-options');
782
+
783
+ select.options = [
784
+ { id: 1, name: 'React', stars: 220000, trending: true },
785
+ { id: 2, name: 'Vue', stars: 207000, trending: false },
786
+ { id: 3, name: 'Angular', stars: 94000, trending: false },
787
+ { id: 4, name: 'Svelte', stars: 76000, trending: true }
788
+ ];
789
+
790
+ // Custom renderer with full HTML control
791
+ select.renderOptionContentCallback = (item, context) => {
792
+ // Context provides: { index, isSelected, isFocused, isMatched, isDisabled }
793
+
794
+ return `
795
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
796
+ <strong>${item.name}</strong>
797
+ <span style="color: #666; font-size: 0.875rem;">⭐ ${(item.stars / 1000).toFixed(0)}k</span>
798
+ ${item.trending ? '<span style="background: #10b981; color: white; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.75rem;">🔥 Trending</span>' : ''}
799
+ </div>
800
+ `;
801
+ };
802
+ </script>
803
+ ```
804
+
805
+ **Context object** (`OptionContentRenderContext`):
806
+ - `index: number` - Index of the option in the filtered list
807
+ - `isSelected: boolean` - Whether the option is currently selected
808
+ - `isFocused: boolean` - Whether the option is currently focused (keyboard navigation)
809
+ - `isMatched: boolean` - Whether the option matches the current search term (navigate mode only)
810
+ - `isDisabled: boolean` - Whether the option is disabled
811
+
812
+ #### Custom Badge Rendering
813
+
814
+ Customize how selected items appear as badges:
815
+
816
+ ```javascript
817
+ const select = document.querySelector('web-multiselect');
818
+
819
+ select.options = [
820
+ { id: 1, name: 'John Doe', role: 'Admin', avatar: '👨‍💼' },
821
+ { id: 2, name: 'Jane Smith', role: 'Developer', avatar: '👩‍💻' },
822
+ { id: 3, name: 'Bob Johnson', role: 'Designer', avatar: '🎨' }
823
+ ];
824
+
825
+ // Custom badge rendering in main badges area
826
+ select.renderBadgeContentCallback = (item, context) => {
827
+ // Compact view in badges area
828
+ return `${item.avatar} ${item.name}`;
829
+ };
830
+
831
+ // Custom rendering for selected items popover (separate callback)
832
+ select.renderSelectionBadgeContentCallback = (item) => {
833
+ // Full details in popover - has more space
834
+ return `
835
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
836
+ <span>${item.avatar}</span>
837
+ <div>
838
+ <div><strong>${item.name}</strong></div>
839
+ <div style="font-size: 0.75rem; color: #666;">${item.role}</div>
840
+ </div>
841
+ </div>
842
+ `;
843
+ };
844
+ ```
845
+
846
+ **Separate Callbacks for Badges vs. Popover:**
847
+ - `renderBadgeContentCallback` - Renders badges in the main badges area (compact display)
848
+ - `renderSelectionBadgeContentCallback` - Renders items in the selected items popover (can be more detailed)
849
+ - If `renderSelectionBadgeContentCallback` is not defined, falls back to `renderBadgeContentCallback`
850
+ - Users can assign the same function to both if identical rendering is desired
851
+
852
+ **Context object** (`BadgeContentRenderContext` for `renderBadgeContentCallback`):
853
+ - `displayMode: BadgesDisplayMode` - Current badges display mode ('pills', 'count', 'compact', 'partial', 'none')
854
+ - `isInPopover: boolean` - Whether the badge is being rendered in the selected items popover (always false for this callback)
855
+
856
+ #### Custom Badge Styling with CSS Classes
857
+
858
+ Add custom CSS classes to badges based on item data for semantic styling:
859
+
860
+ ```javascript
861
+ const select = document.querySelector('web-multiselect');
862
+
863
+ select.options = [
864
+ { id: 1, task: 'Fix security bug', priority: 'urgent' },
865
+ { id: 2, task: 'Update docs', priority: 'normal' },
866
+ { id: 3, task: 'Refactor code', priority: 'low' }
867
+ ];
868
+
869
+ // Add CSS class based on priority
870
+ select.getBadgeClassCallback = (item) => {
871
+ return `badge-${item.priority}`; // Returns 'badge-urgent', 'badge-normal', etc.
872
+ };
873
+
874
+ // Can also return array of classes
875
+ select.getBadgeClassCallback = (item) => {
876
+ const classes = [`badge-${item.priority}`];
877
+ if (item.urgent) classes.push('badge-blink');
878
+ return classes;
879
+ };
880
+ ```
881
+
882
+ Then style with CSS:
883
+
884
+ ```css
885
+ /* Target specific badges with custom classes */
886
+ .badge-urgent {
887
+ --ml-badge-text-bg: #fee2e2;
888
+ --ml-badge-text-color: #dc2626;
889
+ --ml-badge-remove-bg: #dc2626;
890
+ }
891
+
892
+ .badge-normal {
893
+ --ml-badge-text-bg: #dbeafe;
894
+ --ml-badge-text-color: #2563eb;
895
+ --ml-badge-remove-bg: #2563eb;
896
+ }
897
+
898
+ .badge-low {
899
+ --ml-badge-text-bg: #d1fae5;
900
+ --ml-badge-text-color: #059669;
901
+ --ml-badge-remove-bg: #059669;
902
+ }
903
+ ```
904
+
905
+ The callback:
906
+ - Takes the item as a parameter
907
+ - Returns a string (single class) or array of strings (multiple classes)
908
+ - Classes are added to the badge's base `.ml__badge` element
909
+ - Works across all rendering locations (main badges, partial mode, popover)
910
+
911
+ **Separate Class Callbacks for Badges vs. Popover:**
912
+
913
+ Similar to rendering callbacks, you can use different class callbacks for badges and selected items:
914
+
915
+ ```javascript
916
+ // Add classes to badges in main area
917
+ select.getBadgeClassCallback = (item) => {
918
+ return `badge-${item.priority}`;
919
+ };
920
+
921
+ // Add different/additional classes to selected items in popover
922
+ select.getSelectionBadgeClassCallback = (item) => {
923
+ // Could add more detailed classes for popover items
924
+ return [`badge-${item.priority}`, 'badge-detailed'];
925
+ };
926
+ ```
927
+
928
+ - `getBadgeClassCallback` - Adds classes to badges in the main badges area
929
+ - `getSelectionBadgeClassCallback` - Adds classes to items in the selected items popover
930
+ - If `getSelectionBadgeClassCallback` is not defined, falls back to `getBadgeClassCallback`
931
+ - Users can assign the same function to both if identical styling is desired
932
+
933
+ **Shadow DOM CSS Injection:**
934
+
935
+ Since the component uses Shadow DOM, regular page CSS cannot style shadow elements. Use `customStylesCallback` to inject CSS directly into the Shadow DOM:
936
+
937
+ ```javascript
938
+ const select = document.querySelector('web-multiselect');
939
+
940
+ // Add CSS classes to badges based on item data
941
+ select.getBadgeClassCallback = (item) => {
942
+ return `badge-${item.priority}`;
943
+ };
944
+
945
+ // Inject CSS into Shadow DOM to style those classes
946
+ select.customStylesCallback = () => `
947
+ .badge-urgent {
948
+ --ml-badge-text-bg: #fee2e2;
949
+ --ml-badge-text-color: #dc2626;
950
+ --ml-badge-remove-bg: #dc2626;
951
+ }
952
+
953
+ .badge-normal {
954
+ --ml-badge-text-bg: #dbeafe;
955
+ --ml-badge-text-color: #2563eb;
956
+ --ml-badge-remove-bg: #2563eb;
957
+ }
958
+
959
+ .badge-low {
960
+ --ml-badge-text-bg: #d1fae5;
961
+ --ml-badge-text-color: #059669;
962
+ --ml-badge-remove-bg: #059669;
963
+ }
964
+ `;
965
+ ```
966
+
967
+ The `customStylesCallback`:
968
+ - Returns a CSS string (not HTML)
969
+ - Styles are injected into the Shadow DOM on initialization
970
+ - Can be updated dynamically - new styles replace old ones
971
+ - Works with all custom classes (from `getBadgeClassCallback`, `renderOptionContentCallback`, etc.)
972
+
973
+ #### Custom Selected Item Rendering (Single-Select)
974
+
975
+ Customize the text shown in the input field when in single-select mode:
976
+
977
+ ```javascript
978
+ const select = document.querySelector('web-multiselect[multiple="false"]');
979
+
980
+ select.options = [
981
+ { id: 1, firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
982
+ { id: 2, firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' }
983
+ ];
984
+
985
+ // Show just first name when closed
986
+ select.renderSelectedContentCallback = (item) => {
987
+ return item.firstName; // Returns plain text (not HTML)
988
+ };
989
+
990
+ // While dropdown shows full details
991
+ select.getDisplayValueCallback = (item) => {
992
+ return `${item.firstName} ${item.lastName} (${item.email})`;
993
+ };
994
+ ```
995
+
996
+ #### Conditional Rendering Example
997
+
998
+ Use JavaScript logic for conditional rendering:
999
+
1000
+ ```javascript
1001
+ select.renderOptionContentCallback = (item, context) => {
1002
+ const classes = [];
1003
+ if (context.isSelected) classes.push('selected');
1004
+ if (context.isFocused) classes.push('focused');
1005
+
1006
+ return `
1007
+ <div class="${classes.join(' ')}">
1008
+ ${item.isNew ? '<span class="badge-new">NEW</span>' : ''}
1009
+ <strong>${item.name}</strong>
1010
+ ${item.description ? `<p style="font-size: 0.875rem; color: #666;">${item.description}</p>` : ''}
1011
+ ${item.tags ? `<div class="tags">${item.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}</div>` : ''}
1012
+ </div>
1013
+ `;
1014
+ };
1015
+ ```
1016
+
1017
+ #### Returning HTMLElement
1018
+
1019
+ You can also return DOM elements for more complex rendering:
1020
+
1021
+ ```javascript
1022
+ select.renderOptionContentCallback = (item, context) => {
1023
+ const div = document.createElement('div');
1024
+ div.style.display = 'flex';
1025
+ div.style.alignItems = 'center';
1026
+ div.style.gap = '0.5rem';
1027
+
1028
+ const img = document.createElement('img');
1029
+ img.src = item.avatarUrl;
1030
+ img.style.width = '32px';
1031
+ img.style.height = '32px';
1032
+ img.style.borderRadius = '50%';
1033
+
1034
+ const span = document.createElement('span');
1035
+ span.textContent = item.name;
1036
+
1037
+ div.appendChild(img);
1038
+ div.appendChild(span);
1039
+
1040
+ return div; // Return HTMLElement instead of string
1041
+ };
1042
+ ```
1043
+
1044
+ #### Virtual Scroll Compatibility
1045
+
1046
+ When using `renderOptionContentCallback` with virtual scroll enabled:
1047
+
1048
+ ⚠️ **Important**: Custom option content **must fit within** the configured `optionHeight` (default: 50px)
1049
+
1050
+ ```html
1051
+ <web-multiselect
1052
+ id="large-dataset"
1053
+ enable-virtual-scroll="true"
1054
+ option-height="60">
1055
+ </web-multiselect>
1056
+
1057
+ <script type="module">
1058
+ const select = document.getElementById('large-dataset');
1059
+
1060
+ select.renderOptionContentCallback = (item) => {
1061
+ // Content must fit in 60px height
1062
+ return `
1063
+ <div style="height: 60px; display: flex; align-items: center;">
1064
+ <strong>${item.name}</strong>
1065
+ </div>
1066
+ `;
1067
+ };
1068
+ </script>
1069
+ ```
1070
+
1071
+ **Virtual scroll requirements:**
1072
+ - Content height must be **fixed** and match `optionHeight`
1073
+ - Overflow will be clipped
1074
+ - Variable-height content only works in non-virtual mode
1075
+
1076
+ #### Callback Priority
1077
+
1078
+ The component uses a fallback chain when callbacks are not provided:
1079
+
1080
+ **For options:**
1081
+ 1. `renderOptionContentCallback` (full HTML control)
1082
+ 2. Default: icon + `getDisplayValueCallback` + subtitle
1083
+
1084
+ **For badges:**
1085
+ 1. `renderBadgeContentCallback` (full HTML control)
1086
+ 2. `getBadgeDisplayCallback` (text only)
1087
+ 3. `getDisplayValueCallback` (text only)
1088
+
1089
+ **For selected item (single-select):**
1090
+ 1. `renderSelectedContentCallback` (text only)
1091
+ 2. `getDisplayValueCallback` (text only)
1092
+
1093
+ #### Checkbox Control
1094
+
1095
+ Control checkbox appearance and alignment with CSS variables and attributes:
1096
+
1097
+ **Checkbox Alignment (via attribute):**
1098
+ ```html
1099
+ <web-multiselect checkbox-align="top"></web-multiselect> <!-- Default -->
1100
+ <web-multiselect checkbox-align="center"></web-multiselect> <!-- Middle aligned -->
1101
+ <web-multiselect checkbox-align="bottom"></web-multiselect> <!-- Bottom aligned -->
1102
+ ```
1103
+
1104
+ **Checkbox Size/Scale (via CSS):**
1105
+ ```html
1106
+ <style>
1107
+ /* Change checkbox size */
1108
+ web-multiselect {
1109
+ --ml-checkbox-size: 20px; /* Width and height (default: 16px) */
1110
+ }
1111
+
1112
+ /* Scale checkbox */
1113
+ web-multiselect {
1114
+ --ml-checkbox-scale: 1.5; /* Scale multiplier (default: 1) */
1115
+ }
1116
+
1117
+ /* Fine-tune vertical position */
1118
+ web-multiselect {
1119
+ --ml-checkbox-margin-top: 0.5rem; /* Offset from top (default: 0.125rem) */
1120
+ }
1121
+ </style>
1122
+ ```
1123
+
1124
+ **CSS Grid/Flexbox in Custom Content:**
1125
+
1126
+ Custom rendering callbacks support full CSS layout control:
1127
+
1128
+ ```javascript
1129
+ // CSS Grid example
1130
+ multiselect.renderOptionContentCallback = (item, context) => {
1131
+ return `
1132
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;">
1133
+ <div><strong>Name:</strong> ${item.name}</div>
1134
+ <div><strong>Price:</strong> ${item.price}</div>
1135
+ <div><strong>Stock:</strong> ${item.stock}</div>
1136
+ <div><strong>Rating:</strong> ${item.rating}</div>
1137
+ </div>
1138
+ `;
1139
+ };
1140
+
1141
+ // Flexbox example
1142
+ multiselect.renderOptionContentCallback = (item, context) => {
1143
+ return `
1144
+ <div style="display: flex; justify-content: space-between; align-items: center;">
1145
+ <div style="display: flex; flex-direction: column;">
1146
+ <strong>${item.name}</strong>
1147
+ <span style="font-size: 0.875rem; color: #666;">${item.description}</span>
1148
+ </div>
1149
+ <div style="text-align: right;">
1150
+ <div>${item.price}</div>
1151
+ <div style="font-size: 0.875rem;">${item.stock} in stock</div>
1152
+ </div>
1153
+ </div>
1154
+ `;
1155
+ };
1156
+ ```
1157
+
1158
+ **Available CSS Variables:**
1159
+ - `--ml-checkbox-size`: Checkbox width/height (default: `16px`)
1160
+ - `--ml-checkbox-scale`: Scale multiplier (default: `1`)
1161
+ - `--ml-checkbox-margin-top`: Vertical offset (default: `0.125rem`)
1162
+ - `--ml-checkbox-align`: Alignment value (default: `flex-start`)
1163
+ - `--ml-option-gap`: Gap between checkbox and content (default: `0.5rem`)
1164
+
702
1165
  ### Flexible Data Handling
703
1166
 
704
1167
  The component supports **any data structure** through a member/callback pattern, allowing you to work with custom objects, tuple arrays, or existing API responses without transformation.
@@ -773,9 +1236,9 @@ select.getDisabledCallback = (item) => {
773
1236
  return item.stock === 0 || item.discontinued;
774
1237
  };
775
1238
 
776
- // Customize pill display (show different text in pills vs dropdown)
777
- select.getPillDisplayCallback = (item) => {
778
- // Pills show just the name for space efficiency
1239
+ // Customize badge display (show different text in badges vs dropdown)
1240
+ select.getBadgeDisplayCallback = (item) => {
1241
+ // Badges show just the name for space efficiency
779
1242
  return item.name;
780
1243
  // While dropdown can show full details: "Laptop - $999 - Electronics"
781
1244
  };
@@ -1085,30 +1548,32 @@ For the complete list of all available CSS variables, see:
1085
1548
  | `--ml-option-hover-bg` | `#f9fafb` | Option background on hover |
1086
1549
  | `--ml-option-bg-selected` | (rgba accent) | Selected option background |
1087
1550
 
1088
- #### Pills & Badges
1551
+ #### Badges
1089
1552
 
1090
1553
  | Variable | Default | Description |
1091
1554
  |----------|---------|-------------|
1092
- | `--ml-pill-bg` | `#eff6ff` | Pill background color |
1093
- | `--ml-pill-text-color` | `#3b82f6` | Pill text color |
1094
- | `--ml-pill-gap` | `0.5rem` | Gap between pills |
1095
- | `--ml-pill-height` | `1.5rem` | Height of pills |
1096
- | `--ml-pill-font-size` | `0.75rem` | Pill font size |
1097
- | `--ml-pill-border-radius` | `0.375rem` | Pill border radius |
1098
- | `--ml-pill-remove-bg` | `#3b82f6` | Remove button background |
1099
- | `--ml-pill-remove-color` | `#ffffff` | Remove button color |
1100
- | `--ml-more-badge-bg` | (pill background) | "+X more" badge background |
1101
- | `--ml-more-badge-hover-bg` | `#ffffff` | "+X more" badge hover |
1102
- | `--ml-more-badge-active-bg` | `#e0f2fe` | "+X more" badge active |
1103
-
1104
- #### Count Badge (in input)
1555
+ | `--ml-badge-text-bg` | `#eff6ff` | Badge background color |
1556
+ | `--ml-badge-text-color` | `#3b82f6` | Badge text color |
1557
+ | `--ml-badge-gap` | `0.5rem` | Gap between badges |
1558
+ | `--ml-badge-height` | `1.5rem` | Height of badges |
1559
+ | `--ml-badge-font-size` | `0.75rem` | Badge font size |
1560
+ | `--ml-badge-border-radius` | `0.375rem` | Badge border radius |
1561
+ | `--ml-badge-remove-bg` | `#3b82f6` | Remove button background |
1562
+ | `--ml-badge-remove-color` | `#ffffff` | Remove button color |
1563
+ | `--ml-badge-counter-text-bg` | `#d1d5db` | BadgeCounter text background ("+X more") |
1564
+ | `--ml-badge-counter-text-color` | `#6b7280` | BadgeCounter text color |
1565
+ | `--ml-badge-counter-remove-bg` | `#6b7280` | BadgeCounter remove button background |
1566
+ | `--ml-badge-counter-remove-color` | `#ffffff` | BadgeCounter remove button color |
1567
+ | `--ml-badge-counter-border` | `1px solid #e5e7eb` | BadgeCounter border |
1568
+
1569
+ #### Counter (in input)
1105
1570
 
1106
1571
  | Variable | Default | Description |
1107
1572
  |----------|---------|-------------|
1108
- | `--ml-count-badge-bg` | `#3b82f6` | Count badge background |
1109
- | `--ml-count-badge-color` | `#ffffff` | Count badge text color |
1110
- | `--ml-count-badge-font-size` | `0.75rem` | Count badge font size |
1111
- | `--ml-count-badge-bg-hover` | `#2563eb` | Hover background color |
1573
+ | `--ml-counter-bg` | `#3b82f6` | Counter background |
1574
+ | `--ml-counter-color` | `#ffffff` | Counter text color |
1575
+ | `--ml-counter-font-size` | `0.75rem` | Counter font size |
1576
+ | `--ml-counter-bg-hover` | `#2563eb` | Hover background color |
1112
1577
 
1113
1578
  #### Tooltips
1114
1579