0
# Custom Rendering
1
2
Comprehensive slot system enabling complete customization of all UI elements including options, tags, labels, and controls.
3
4
## Capabilities
5
6
### Basic Slots
7
8
Essential slots for customizing core component elements.
9
10
```typescript { .api }
11
/**
12
* Basic customization slots
13
*/
14
interface BasicCustomizationSlots {
15
/** Custom dropdown caret/arrow icon */
16
caret: { toggle: () => void };
17
18
/** Custom clear button */
19
clear: { search: string };
20
21
/** Custom placeholder content */
22
placeholder: {};
23
24
/** Custom loading indicator */
25
loading: {};
26
}
27
```
28
29
**Usage Example:**
30
31
```vue
32
<template>
33
<VueMultiselect
34
v-model="selectedOption"
35
:options="options"
36
:loading="isLoading"
37
placeholder="Custom styled select">
38
39
<template #caret="{ toggle }">
40
<button @click="toggle" class="custom-caret">
41
<i class="icon-chevron-down"></i>
42
</button>
43
</template>
44
45
<template #clear="{ search }">
46
<button v-if="search" @click="clearSearch" class="custom-clear">
47
<i class="icon-x"></i>
48
</button>
49
</template>
50
51
<template #placeholder>
52
<span class="custom-placeholder">
53
<i class="icon-search"></i>
54
Choose an option...
55
</span>
56
</template>
57
58
<template #loading>
59
<div class="custom-loading">
60
<div class="spinner"></div>
61
<span>Loading options...</span>
62
</div>
63
</template>
64
</VueMultiselect>
65
</template>
66
67
<style>
68
.custom-caret {
69
background: none;
70
border: none;
71
padding: 8px;
72
cursor: pointer;
73
}
74
75
.custom-clear {
76
background: #ff4757;
77
color: white;
78
border: none;
79
border-radius: 50%;
80
width: 20px;
81
height: 20px;
82
cursor: pointer;
83
}
84
85
.custom-placeholder {
86
display: flex;
87
align-items: center;
88
gap: 8px;
89
color: #999;
90
}
91
92
.custom-loading {
93
display: flex;
94
align-items: center;
95
gap: 8px;
96
padding: 8px;
97
}
98
99
.spinner {
100
width: 16px;
101
height: 16px;
102
border: 2px solid #f3f3f3;
103
border-top: 2px solid #3498db;
104
border-radius: 50%;
105
animation: spin 1s linear infinite;
106
}
107
108
@keyframes spin {
109
0% { transform: rotate(0deg); }
110
100% { transform: rotate(360deg); }
111
}
112
</style>
113
```
114
115
### Selection Display Slots
116
117
Customize how selected values are displayed.
118
119
```typescript { .api }
120
/**
121
* Selection display customization slots
122
*/
123
interface SelectionDisplaySlots {
124
/**
125
* Complete selection area customization
126
* Replaces default tag display and single value display
127
*/
128
selection: {
129
search: string;
130
remove: (option: any) => void;
131
values: any[];
132
isOpen: boolean;
133
};
134
135
/** Custom individual tag rendering for multiple selection */
136
tag: {
137
option: any;
138
search: string;
139
remove: (option: any) => void;
140
};
141
142
/** Custom single value label for single selection */
143
singleLabel: {
144
option: any;
145
};
146
147
/** Custom text for selection limits */
148
limit: {};
149
}
150
```
151
152
**Usage Example:**
153
154
```vue
155
<template>
156
<VueMultiselect
157
v-model="selectedUsers"
158
:options="users"
159
:multiple="true"
160
:limit="3"
161
label="name"
162
track-by="id">
163
164
<template #selection="{ values, remove, isOpen }">
165
<div class="custom-selection">
166
<div class="selected-count">
167
{{ values.length }} user{{ values.length === 1 ? '' : 's' }} selected
168
</div>
169
<div v-if="isOpen" class="selection-details">
170
<div v-for="user in values" :key="user.id" class="selected-user">
171
<img :src="user.avatar" :alt="user.name" class="user-avatar">
172
<span>{{ user.name }}</span>
173
<button @click="remove(user)" class="remove-user">×</button>
174
</div>
175
</div>
176
</div>
177
</template>
178
179
<template #tag="{ option, remove }">
180
<div class="user-tag">
181
<img :src="option.avatar" :alt="option.name" class="tag-avatar">
182
<span class="tag-name">{{ option.name }}</span>
183
<span class="tag-role">{{ option.role }}</span>
184
<button @click="remove(option)" class="tag-remove">×</button>
185
</div>
186
</template>
187
188
<template #singleLabel="{ option }">
189
<div class="single-user-label">
190
<img :src="option.avatar" :alt="option.name" class="single-avatar">
191
<div class="single-user-info">
192
<div class="single-user-name">{{ option.name }}</div>
193
<div class="single-user-role">{{ option.role }}</div>
194
</div>
195
</div>
196
</template>
197
198
<template #limit>
199
<span class="custom-limit-text">
200
<i class="icon-more"></i>
201
and more...
202
</span>
203
</template>
204
</VueMultiselect>
205
</template>
206
207
<style>
208
.custom-selection {
209
padding: 8px;
210
background: #f8f9fa;
211
border-radius: 4px;
212
}
213
214
.selected-count {
215
font-weight: 500;
216
color: #495057;
217
}
218
219
.selection-details {
220
margin-top: 8px;
221
display: flex;
222
flex-wrap: wrap;
223
gap: 8px;
224
}
225
226
.selected-user {
227
display: flex;
228
align-items: center;
229
gap: 6px;
230
padding: 4px 8px;
231
background: white;
232
border-radius: 16px;
233
font-size: 12px;
234
}
235
236
.user-avatar, .tag-avatar, .single-avatar {
237
width: 24px;
238
height: 24px;
239
border-radius: 50%;
240
object-fit: cover;
241
}
242
243
.user-tag {
244
display: flex;
245
align-items: center;
246
gap: 6px;
247
padding: 4px 8px;
248
background: #e3f2fd;
249
border-radius: 16px;
250
font-size: 12px;
251
}
252
253
.tag-role {
254
color: #666;
255
font-size: 10px;
256
}
257
258
.single-user-label {
259
display: flex;
260
align-items: center;
261
gap: 8px;
262
}
263
264
.single-user-info {
265
display: flex;
266
flex-direction: column;
267
}
268
269
.single-user-name {
270
font-weight: 500;
271
}
272
273
.single-user-role {
274
font-size: 12px;
275
color: #666;
276
}
277
</style>
278
```
279
280
### Option Display Slots
281
282
Customize how options appear in the dropdown.
283
284
```typescript { .api }
285
/**
286
* Option display customization slots
287
*/
288
interface OptionDisplaySlots {
289
/** Custom option rendering */
290
option: {
291
option: any;
292
search: string;
293
index: number;
294
};
295
296
/** Content before the options list */
297
beforeList: {};
298
299
/** Content after the options list */
300
afterList: {};
301
302
/** Custom message when no options available */
303
noOptions: {};
304
305
/** Custom message when search yields no results */
306
noResult: {
307
search: string;
308
};
309
310
/** Custom message when maximum selections reached */
311
maxElements: {};
312
}
313
```
314
315
**Usage Example:**
316
317
```vue
318
<template>
319
<VueMultiselect
320
v-model="selectedProduct"
321
:options="products"
322
:searchable="true"
323
label="name"
324
track-by="id">
325
326
<template #beforeList>
327
<div class="product-header">
328
<h4>Available Products</h4>
329
<div class="product-stats">
330
{{ products.length }} products available
331
</div>
332
</div>
333
</template>
334
335
<template #option="{ option, search, index }">
336
<div class="product-option" :class="{ 'featured': option.featured }">
337
<img :src="option.image" :alt="option.name" class="product-image">
338
<div class="product-info">
339
<div class="product-name">
340
<highlightText :text="option.name" :query="search" />
341
</div>
342
<div class="product-price">${{ option.price }}</div>
343
<div class="product-category">{{ option.category }}</div>
344
<div class="product-rating">
345
<span class="stars">{{ '★'.repeat(option.rating) }}</span>
346
<span class="rating-text">({{ option.reviews }} reviews)</span>
347
</div>
348
</div>
349
<div class="product-actions">
350
<span v-if="option.inStock" class="stock-status in-stock">In Stock</span>
351
<span v-else class="stock-status out-of-stock">Out of Stock</span>
352
<button v-if="option.featured" class="featured-badge">Featured</button>
353
</div>
354
</div>
355
</template>
356
357
<template #afterList>
358
<div class="product-footer">
359
<a href="/products" class="view-all-link">
360
View all products →
361
</a>
362
</div>
363
</template>
364
365
<template #noOptions>
366
<div class="no-products">
367
<i class="icon-package"></i>
368
<h3>No products available</h3>
369
<p>Please check back later or contact support.</p>
370
<button @click="refreshProducts">Refresh</button>
371
</div>
372
</template>
373
374
<template #noResult="{ search }">
375
<div class="no-search-results">
376
<i class="icon-search"></i>
377
<h3>No products found</h3>
378
<p>No products match "{{ search }}"</p>
379
<div class="search-suggestions">
380
<p>Try searching for:</p>
381
<button
382
v-for="suggestion in searchSuggestions"
383
:key="suggestion"
384
@click="searchForSuggestion(suggestion)"
385
class="suggestion-button">
386
{{ suggestion }}
387
</button>
388
</div>
389
</div>
390
</template>
391
392
<template #maxElements>
393
<div class="max-selection-message">
394
<i class="icon-warning"></i>
395
<p>Maximum number of products selected</p>
396
<p>Remove a product to select another</p>
397
</div>
398
</template>
399
</VueMultiselect>
400
</template>
401
402
<script>
403
export default {
404
data() {
405
return {
406
selectedProduct: null,
407
products: [
408
{
409
id: 1,
410
name: 'Premium Laptop',
411
price: 1299,
412
category: 'Electronics',
413
image: '/images/laptop.jpg',
414
rating: 5,
415
reviews: 124,
416
inStock: true,
417
featured: true
418
}
419
],
420
searchSuggestions: ['laptop', 'phone', 'tablet', 'headphones']
421
}
422
},
423
methods: {
424
refreshProducts() {
425
// Refresh products logic
426
},
427
428
searchForSuggestion(suggestion) {
429
// Set search to suggestion
430
this.$refs.multiselect.updateSearch(suggestion);
431
}
432
}
433
}
434
</script>
435
436
<style>
437
.product-header {
438
padding: 12px;
439
border-bottom: 1px solid #e9ecef;
440
background: #f8f9fa;
441
}
442
443
.product-stats {
444
font-size: 12px;
445
color: #666;
446
}
447
448
.product-option {
449
display: flex;
450
align-items: center;
451
padding: 12px;
452
gap: 12px;
453
border-bottom: 1px solid #f0f0f0;
454
}
455
456
.product-option.featured {
457
background: linear-gradient(90deg, #fff3cd 0%, #ffffff 100%);
458
}
459
460
.product-image {
461
width: 48px;
462
height: 48px;
463
object-fit: cover;
464
border-radius: 4px;
465
}
466
467
.product-info {
468
flex: 1;
469
}
470
471
.product-name {
472
font-weight: 500;
473
margin-bottom: 4px;
474
}
475
476
.product-price {
477
font-size: 14px;
478
color: #28a745;
479
font-weight: 600;
480
}
481
482
.product-category {
483
font-size: 12px;
484
color: #666;
485
}
486
487
.product-rating {
488
font-size: 12px;
489
margin-top: 4px;
490
}
491
492
.stars {
493
color: #ffc107;
494
}
495
496
.rating-text {
497
color: #666;
498
margin-left: 4px;
499
}
500
501
.product-actions {
502
display: flex;
503
flex-direction: column;
504
align-items: flex-end;
505
gap: 4px;
506
}
507
508
.stock-status {
509
font-size: 11px;
510
padding: 2px 6px;
511
border-radius: 10px;
512
}
513
514
.stock-status.in-stock {
515
background: #d4edda;
516
color: #155724;
517
}
518
519
.stock-status.out-of-stock {
520
background: #f8d7da;
521
color: #721c24;
522
}
523
524
.featured-badge {
525
background: #ff6b6b;
526
color: white;
527
border: none;
528
padding: 2px 6px;
529
border-radius: 10px;
530
font-size: 10px;
531
}
532
533
.product-footer {
534
padding: 12px;
535
text-align: center;
536
border-top: 1px solid #e9ecef;
537
background: #f8f9fa;
538
}
539
540
.view-all-link {
541
color: #007bff;
542
text-decoration: none;
543
font-size: 14px;
544
}
545
546
.no-products, .no-search-results, .max-selection-message {
547
padding: 24px;
548
text-align: center;
549
}
550
551
.search-suggestions {
552
margin-top: 16px;
553
}
554
555
.suggestion-button {
556
margin: 4px;
557
padding: 4px 8px;
558
border: 1px solid #ddd;
559
background: white;
560
border-radius: 16px;
561
cursor: pointer;
562
font-size: 12px;
563
}
564
</style>
565
```
566
567
### Advanced Slot Combinations
568
569
Combine multiple slots for complex custom layouts.
570
571
```vue
572
<template>
573
<VueMultiselect
574
v-model="selectedTeamMembers"
575
:options="teamMembers"
576
:multiple="true"
577
:searchable="true"
578
label="name"
579
track-by="id"
580
class="team-selector">
581
582
<!-- Custom header with stats -->
583
<template #beforeList>
584
<div class="team-stats-header">
585
<div class="stat">
586
<span class="stat-number">{{ teamMembers.length }}</span>
587
<span class="stat-label">Available</span>
588
</div>
589
<div class="stat">
590
<span class="stat-number">{{ selectedTeamMembers.length }}</span>
591
<span class="stat-label">Selected</span>
592
</div>
593
<div class="stat">
594
<span class="stat-number">{{ onlineMembers }}</span>
595
<span class="stat-label">Online</span>
596
</div>
597
</div>
598
</template>
599
600
<!-- Custom option with rich member info -->
601
<template #option="{ option, search }">
602
<div class="member-option">
603
<div class="member-avatar-container">
604
<img :src="option.avatar" :alt="option.name" class="member-avatar">
605
<div class="status-indicator" :class="option.status"></div>
606
</div>
607
608
<div class="member-details">
609
<div class="member-name">
610
<highlightText :text="option.name" :query="search" />
611
</div>
612
<div class="member-role">{{ option.role }}</div>
613
<div class="member-department">{{ option.department }}</div>
614
</div>
615
616
<div class="member-metrics">
617
<div class="metric">
618
<span class="metric-value">{{ option.tasksCount }}</span>
619
<span class="metric-label">Tasks</span>
620
</div>
621
<div class="metric">
622
<span class="metric-value">{{ option.availability }}%</span>
623
<span class="metric-label">Available</span>
624
</div>
625
</div>
626
627
<div class="member-actions">
628
<button @click.stop="viewProfile(option)" class="action-btn">
629
<i class="icon-user"></i>
630
</button>
631
<button @click.stop="sendMessage(option)" class="action-btn">
632
<i class="icon-message"></i>
633
</button>
634
</div>
635
</div>
636
</template>
637
638
<!-- Custom selection display with team composition -->
639
<template #selection="{ values, remove, isOpen }">
640
<div class="team-composition">
641
<!-- Role distribution chart -->
642
<div class="role-distribution">
643
<div
644
v-for="role in roleDistribution"
645
:key="role.name"
646
class="role-segment"
647
:style="{ width: role.percentage + '%', backgroundColor: role.color }">
648
</div>
649
</div>
650
651
<!-- Selected members preview -->
652
<div class="selected-members-preview">
653
<div
654
v-for="member in values.slice(0, 5)"
655
:key="member.id"
656
class="member-preview">
657
<img :src="member.avatar" :alt="member.name" class="preview-avatar">
658
</div>
659
<div v-if="values.length > 5" class="more-members">
660
+{{ values.length - 5 }}
661
</div>
662
</div>
663
664
<!-- Team stats -->
665
<div class="team-summary">
666
<span class="team-size">{{ values.length }} members</span>
667
<span class="team-capacity">{{ totalCapacity }}% capacity</span>
668
</div>
669
670
<!-- Expandable details when open -->
671
<div v-if="isOpen" class="expanded-selection">
672
<div v-for="member in values" :key="member.id" class="selected-member-detail">
673
<img :src="member.avatar" :alt="member.name">
674
<div class="member-info">
675
<span class="name">{{ member.name }}</span>
676
<span class="role">{{ member.role }}</span>
677
</div>
678
<button @click="remove(member)" class="remove-member">×</button>
679
</div>
680
</div>
681
</div>
682
</template>
683
684
<!-- Custom footer with team actions -->
685
<template #afterList>
686
<div class="team-actions-footer">
687
<button @click="selectByRole('developer')" class="role-select-btn">
688
Select All Developers
689
</button>
690
<button @click="selectByRole('designer')" class="role-select-btn">
691
Select All Designers
692
</button>
693
<button @click="selectOnlineMembers" class="role-select-btn">
694
Select Online Members
695
</button>
696
</div>
697
</template>
698
</VueMultiselect>
699
</template>
700
701
<script>
702
export default {
703
computed: {
704
onlineMembers() {
705
return this.teamMembers.filter(member => member.status === 'online').length;
706
},
707
708
roleDistribution() {
709
const roles = {};
710
this.selectedTeamMembers.forEach(member => {
711
roles[member.role] = (roles[member.role] || 0) + 1;
712
});
713
714
const total = this.selectedTeamMembers.length;
715
const colors = {
716
'developer': '#4CAF50',
717
'designer': '#2196F3',
718
'manager': '#FF9800',
719
'analyst': '#9C27B0'
720
};
721
722
return Object.entries(roles).map(([role, count]) => ({
723
name: role,
724
count,
725
percentage: (count / total) * 100,
726
color: colors[role] || '#999'
727
}));
728
},
729
730
totalCapacity() {
731
return this.selectedTeamMembers
732
.reduce((total, member) => total + member.availability, 0);
733
}
734
},
735
736
methods: {
737
viewProfile(member) {
738
// Navigate to member profile
739
},
740
741
sendMessage(member) {
742
// Open messaging interface
743
},
744
745
selectByRole(role) {
746
const roleMembers = this.teamMembers.filter(member => member.role === role);
747
this.selectedTeamMembers = [...this.selectedTeamMembers, ...roleMembers];
748
},
749
750
selectOnlineMembers() {
751
const onlineMembers = this.teamMembers.filter(member => member.status === 'online');
752
this.selectedTeamMembers = onlineMembers;
753
}
754
}
755
}
756
</script>
757
```
758
759
### Slot Prop Reference
760
761
Complete reference of all available slot props.
762
763
```typescript { .api }
764
/**
765
* Complete slot prop interfaces
766
*/
767
interface AllSlots {
768
// Control slots
769
caret: { toggle: () => void };
770
clear: { search: string };
771
772
// Selection slots
773
selection: {
774
search: string;
775
remove: (option: any) => void;
776
values: any[];
777
isOpen: boolean
778
};
779
tag: { option: any; search: string; remove: (option: any) => void };
780
singleLabel: { option: any };
781
limit: {};
782
783
// Content slots
784
placeholder: {};
785
loading: {};
786
787
// List slots
788
beforeList: {};
789
afterList: {};
790
option: { option: any; search: string; index: number };
791
792
// Message slots
793
noOptions: {};
794
noResult: { search: string };
795
maxElements: {};
796
}
797
```