0
# Customization and Styling
1
2
Extensive customization options through slots, component overrides, SCSS variables, and custom positioning for advanced use cases.
3
4
## Capabilities
5
6
### Display and Appearance
7
8
Properties that control the visual appearance and behavior of the component.
9
10
```javascript { .api }
11
/**
12
* Placeholder text displayed when no option is selected
13
*/
14
placeholder: String // default: ''
15
16
/**
17
* Sets a Vue transition property on the dropdown menu
18
* Controls dropdown open/close animation
19
*/
20
transition: String // default: 'vs__fade'
21
22
/**
23
* Sets RTL (right-to-left) support
24
* Accepts 'ltr', 'rtl', or 'auto'
25
*/
26
dir: String // default: 'auto'
27
28
/**
29
* Sets the id attribute of the input element
30
* Useful for accessibility and form integration
31
*/
32
inputId: String // default: undefined
33
34
/**
35
* Unique identifier used to generate IDs in HTML
36
* Must be unique for every instance of vue-select
37
*/
38
uid: String | Number // default: uniqueId()
39
```
40
41
### Component Override System
42
43
Advanced customization through component replacement and positioning.
44
45
```javascript { .api }
46
/**
47
* Object with custom components to overwrite default implementations
48
* Keys are merged with defaults, allowing selective overrides
49
*/
50
components: Object // default: {}
51
52
/**
53
* Append the dropdown element to the end of the body
54
* Enables advanced positioning and z-index control
55
*/
56
appendToBody: Boolean // default: false
57
58
/**
59
* Custom positioning function when appendToBody is true
60
* Responsible for positioning the dropdown list dynamically
61
* @param dropdownList - The dropdown DOM element
62
* @param component - Vue Select component instance
63
* @param styles - Calculated position styles
64
* @returns Cleanup function
65
*/
66
calculatePosition: Function // default: built-in positioning logic
67
68
/**
69
* Determines whether the dropdown should be open
70
* Allows custom dropdown open/close logic
71
* @param instance - Vue Select component instance
72
* @returns Whether dropdown should be open
73
*/
74
dropdownShouldOpen: Function // default: standard open logic
75
76
/**
77
* Disable the dropdown entirely
78
* Component becomes a read-only display
79
*/
80
noDrop: Boolean // default: false
81
```
82
83
### Slot System
84
85
Comprehensive slot-based customization for all UI elements.
86
87
```javascript { .api }
88
/**
89
* Available scoped slots for complete UI customization
90
*/
91
slots: {
92
/**
93
* Content displayed before the dropdown toggle area
94
* @param scope.header - Header-specific data
95
*/
96
'header': { scope: { header: Object } },
97
98
/**
99
* Container around each selected option
100
* @param option - The selected option object
101
* @param deselect - Function to deselect this option
102
* @param multiple - Whether multiple selection is enabled
103
* @param disabled - Whether component is disabled
104
*/
105
'selected-option-container': {
106
option: Object,
107
deselect: Function,
108
multiple: Boolean,
109
disabled: Boolean
110
},
111
112
/**
113
* Individual selected option display
114
* @param normalizeOptionForSlot(option) - Normalized option data
115
*/
116
'selected-option': { /* normalized option properties */ },
117
118
/**
119
* Custom search input implementation
120
* @param scope.search - Search-specific data and methods
121
*/
122
'search': { scope: { search: Object } },
123
124
/**
125
* Custom dropdown open/close indicator
126
* @param scope.openIndicator - Indicator-specific data
127
*/
128
'open-indicator': { scope: { openIndicator: Object } },
129
130
/**
131
* Custom loading spinner
132
* @param scope.spinner - Spinner-specific data
133
*/
134
'spinner': { scope: { spinner: Object } },
135
136
/**
137
* Content at the top of the dropdown list
138
* @param scope.listHeader - List header data
139
*/
140
'list-header': { scope: { listHeader: Object } },
141
142
/**
143
* Individual dropdown option display
144
* @param normalizeOptionForSlot(option) - Normalized option data
145
*/
146
'option': { /* normalized option properties */ },
147
148
/**
149
* Message displayed when no options are available
150
* @param scope.noOptions - No options data
151
*/
152
'no-options': { scope: { noOptions: Object } },
153
154
/**
155
* Content at the bottom of the dropdown list
156
* @param scope.listFooter - List footer data
157
*/
158
'list-footer': { scope: { listFooter: Object } },
159
160
/**
161
* Content displayed after the dropdown area
162
* @param scope.footer - Footer-specific data
163
*/
164
'footer': { scope: { footer: Object } }
165
}
166
```
167
168
### State Classes
169
170
CSS classes automatically applied based on component state.
171
172
```javascript { .api }
173
computed: {
174
/**
175
* Object containing current state CSS classes
176
* Applied to the root component element
177
*/
178
stateClasses: {
179
'vs--open': Boolean, // Dropdown is open
180
'vs--single': Boolean, // Single selection mode
181
'vs--multiple': Boolean, // Multiple selection mode
182
'vs--searchable': Boolean, // Search is enabled
183
'vs--unsearchable': Boolean, // Search is disabled
184
'vs--loading': Boolean, // Loading state active
185
'vs--disabled': Boolean, // Component is disabled
186
'vs--rtl': Boolean // Right-to-left text direction
187
}
188
}
189
```
190
191
## Usage Examples
192
193
### Basic Styling Customization
194
195
```vue
196
<template>
197
<v-select
198
v-model="selected"
199
:options="options"
200
placeholder="Custom styled select..."
201
class="custom-select"
202
/>
203
</template>
204
205
<script>
206
export default {
207
data() {
208
return {
209
selected: null,
210
options: ['Option 1', 'Option 2', 'Option 3']
211
};
212
}
213
};
214
</script>
215
216
<style>
217
.custom-select .vs__dropdown-toggle {
218
border: 2px solid #3498db;
219
border-radius: 8px;
220
}
221
222
.custom-select .vs__selected {
223
background-color: #3498db;
224
color: white;
225
border-radius: 4px;
226
}
227
228
.custom-select .vs__dropdown-menu {
229
border: 2px solid #3498db;
230
border-radius: 8px;
231
}
232
</style>
233
```
234
235
### Custom Selected Option Display
236
237
```vue
238
<template>
239
<v-select
240
v-model="selectedUser"
241
:options="users"
242
label="name"
243
placeholder="Select user..."
244
>
245
<template #selected-option="{ name, email, avatar }">
246
<div class="user-option">
247
<img :src="avatar" :alt="name" class="user-avatar" />
248
<div>
249
<div class="user-name">{{ name }}</div>
250
<div class="user-email">{{ email }}</div>
251
</div>
252
</div>
253
</template>
254
</v-select>
255
</template>
256
257
<script>
258
export default {
259
data() {
260
return {
261
selectedUser: null,
262
users: [
263
{
264
name: 'John Doe',
265
email: 'john@example.com',
266
avatar: 'https://via.placeholder.com/32'
267
},
268
{
269
name: 'Jane Smith',
270
email: 'jane@example.com',
271
avatar: 'https://via.placeholder.com/32'
272
}
273
]
274
};
275
}
276
};
277
</script>
278
279
<style scoped>
280
.user-option {
281
display: flex;
282
align-items: center;
283
gap: 8px;
284
}
285
.user-avatar {
286
width: 32px;
287
height: 32px;
288
border-radius: 50%;
289
}
290
.user-name {
291
font-weight: bold;
292
}
293
.user-email {
294
font-size: 0.8em;
295
color: #666;
296
}
297
</style>
298
```
299
300
### Custom Dropdown Options
301
302
```vue
303
<template>
304
<v-select
305
v-model="selectedProduct"
306
:options="products"
307
label="name"
308
placeholder="Select product..."
309
>
310
<template #option="{ name, price, image, inStock }">
311
<div class="product-option" :class="{ 'out-of-stock': !inStock }">
312
<img :src="image" :alt="name" class="product-image" />
313
<div class="product-info">
314
<div class="product-name">{{ name }}</div>
315
<div class="product-price">${{ price }}</div>
316
<div v-if="!inStock" class="stock-status">Out of Stock</div>
317
</div>
318
</div>
319
</template>
320
</v-select>
321
</template>
322
323
<script>
324
export default {
325
data() {
326
return {
327
selectedProduct: null,
328
products: [
329
{
330
name: 'Laptop',
331
price: 999,
332
image: 'https://via.placeholder.com/40',
333
inStock: true
334
},
335
{
336
name: 'Phone',
337
price: 699,
338
image: 'https://via.placeholder.com/40',
339
inStock: false
340
}
341
]
342
};
343
}
344
};
345
</script>
346
347
<style scoped>
348
.product-option {
349
display: flex;
350
align-items: center;
351
gap: 10px;
352
padding: 5px 0;
353
}
354
.product-option.out-of-stock {
355
opacity: 0.6;
356
}
357
.product-image {
358
width: 40px;
359
height: 40px;
360
border-radius: 4px;
361
}
362
.product-name {
363
font-weight: bold;
364
}
365
.product-price {
366
color: #2ecc71;
367
}
368
.stock-status {
369
color: #e74c3c;
370
font-size: 0.8em;
371
}
372
</style>
373
```
374
375
### Custom Open Indicator
376
377
```vue
378
<template>
379
<v-select
380
v-model="selected"
381
:options="options"
382
placeholder="Custom indicator..."
383
>
384
<template #open-indicator="{ attributes }">
385
<span v-bind="attributes" class="custom-indicator">
386
{{ open ? '▲' : '▼' }}
387
</span>
388
</template>
389
</v-select>
390
</template>
391
392
<script>
393
export default {
394
data() {
395
return {
396
selected: null,
397
options: ['Option 1', 'Option 2', 'Option 3']
398
};
399
}
400
};
401
</script>
402
403
<style scoped>
404
.custom-indicator {
405
font-size: 12px;
406
color: #3498db;
407
transition: transform 0.2s;
408
}
409
</style>
410
```
411
412
### Custom Search Input
413
414
```vue
415
<template>
416
<v-select
417
v-model="selected"
418
:options="options"
419
placeholder="Custom search..."
420
>
421
<template #search="{ attributes, events }">
422
<input
423
v-bind="attributes"
424
v-on="events"
425
class="custom-search"
426
placeholder="🔍 Type to search..."
427
/>
428
</template>
429
</v-select>
430
</template>
431
432
<script>
433
export default {
434
data() {
435
return {
436
selected: null,
437
options: ['Apple', 'Banana', 'Cherry', 'Date']
438
};
439
}
440
};
441
</script>
442
443
<style scoped>
444
.custom-search {
445
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
446
color: white;
447
border: none;
448
padding: 8px 12px;
449
border-radius: 4px;
450
}
451
.custom-search::placeholder {
452
color: rgba(255, 255, 255, 0.7);
453
}
454
</style>
455
```
456
457
### Custom Loading Spinner
458
459
```vue
460
<template>
461
<v-select
462
v-model="selected"
463
:options="options"
464
:loading="isLoading"
465
placeholder="Custom loading..."
466
>
467
<template #spinner>
468
<div class="custom-spinner">
469
<div class="bounce1"></div>
470
<div class="bounce2"></div>
471
<div class="bounce3"></div>
472
</div>
473
</template>
474
</v-select>
475
</template>
476
477
<script>
478
export default {
479
data() {
480
return {
481
selected: null,
482
options: [],
483
isLoading: false
484
};
485
},
486
methods: {
487
async loadOptions() {
488
this.isLoading = true;
489
// Simulate API call
490
await new Promise(resolve => setTimeout(resolve, 2000));
491
this.options = ['Loaded Option 1', 'Loaded Option 2'];
492
this.isLoading = false;
493
}
494
},
495
mounted() {
496
this.loadOptions();
497
}
498
};
499
</script>
500
501
<style scoped>
502
.custom-spinner {
503
display: flex;
504
justify-content: center;
505
align-items: center;
506
gap: 2px;
507
}
508
.custom-spinner > div {
509
width: 6px;
510
height: 6px;
511
background-color: #3498db;
512
border-radius: 100%;
513
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
514
}
515
.bounce1 { animation-delay: -0.32s; }
516
.bounce2 { animation-delay: -0.16s; }
517
@keyframes sk-bouncedelay {
518
0%, 80%, 100% { transform: scale(0); }
519
40% { transform: scale(1.0); }
520
}
521
</style>
522
```
523
524
### Component Override System
525
526
```vue
527
<template>
528
<v-select
529
v-model="selected"
530
:options="options"
531
:components="customComponents"
532
placeholder="Custom components..."
533
/>
534
</template>
535
536
<script>
537
import CustomDeselect from './CustomDeselect.vue';
538
import CustomOpenIndicator from './CustomOpenIndicator.vue';
539
540
export default {
541
data() {
542
return {
543
selected: null,
544
options: ['Option 1', 'Option 2', 'Option 3'],
545
customComponents: {
546
Deselect: CustomDeselect,
547
OpenIndicator: CustomOpenIndicator
548
}
549
};
550
}
551
};
552
</script>
553
```
554
555
### Append to Body with Custom Positioning
556
557
```vue
558
<template>
559
<div class="container">
560
<v-select
561
v-model="selected"
562
:options="options"
563
:appendToBody="true"
564
:calculatePosition="customPosition"
565
placeholder="Dropdown appended to body..."
566
/>
567
</div>
568
</template>
569
570
<script>
571
export default {
572
data() {
573
return {
574
selected: null,
575
options: Array.from({ length: 20 }, (_, i) => `Option ${i + 1}`)
576
};
577
},
578
methods: {
579
customPosition(dropdownList, component, { width, left, top }) {
580
// Custom positioning logic
581
dropdownList.style.position = 'absolute';
582
dropdownList.style.width = width;
583
dropdownList.style.left = left;
584
dropdownList.style.top = top;
585
dropdownList.style.zIndex = '9999';
586
587
// Return cleanup function
588
return () => {
589
dropdownList.style.position = '';
590
dropdownList.style.width = '';
591
dropdownList.style.left = '';
592
dropdownList.style.top = '';
593
dropdownList.style.zIndex = '';
594
};
595
}
596
}
597
};
598
</script>
599
600
<style scoped>
601
.container {
602
height: 200px;
603
overflow: hidden;
604
border: 1px solid #ddd;
605
padding: 20px;
606
}
607
</style>
608
```
609
610
### RTL (Right-to-Left) Support
611
612
```vue
613
<template>
614
<div>
615
<button @click="toggleDirection">
616
Toggle Direction (Current: {{ direction }})
617
</button>
618
619
<v-select
620
v-model="selected"
621
:options="options"
622
:dir="direction"
623
placeholder="RTL/LTR support..."
624
/>
625
</div>
626
</template>
627
628
<script>
629
export default {
630
data() {
631
return {
632
selected: null,
633
direction: 'ltr',
634
options: ['خيار 1', 'خيار 2', 'خيار 3'] // Arabic options
635
};
636
},
637
methods: {
638
toggleDirection() {
639
this.direction = this.direction === 'ltr' ? 'rtl' : 'ltr';
640
}
641
}
642
};
643
</script>
644
```
645
646
### Complete Customization Example
647
648
```vue
649
<template>
650
<v-select
651
v-model="selectedTeamMember"
652
:options="teamMembers"
653
:components="customComponents"
654
label="name"
655
placeholder="Select team member..."
656
class="team-select"
657
>
658
<template #header>
659
<div class="select-header">
660
<h4>Team Members</h4>
661
</div>
662
</template>
663
664
<template #selected-option="member">
665
<div class="selected-member">
666
<img :src="member.avatar" :alt="member.name" />
667
<span>{{ member.name }}</span>
668
<span class="role">{{ member.role }}</span>
669
</div>
670
</template>
671
672
<template #option="member">
673
<div class="member-option" :class="{ offline: !member.online }">
674
<img :src="member.avatar" :alt="member.name" />
675
<div class="member-details">
676
<div class="member-name">{{ member.name }}</div>
677
<div class="member-role">{{ member.role }}</div>
678
<div class="member-status">
679
<span class="status-dot" :class="{ online: member.online }"></span>
680
{{ member.online ? 'Online' : 'Offline' }}
681
</div>
682
</div>
683
</div>
684
</template>
685
686
<template #no-options>
687
<div class="no-members">
688
No team members found. Try adjusting your search.
689
</div>
690
</template>
691
692
<template #footer>
693
<div class="select-footer">
694
<small>{{ teamMembers.length }} team members total</small>
695
</div>
696
</template>
697
</v-select>
698
</template>
699
700
<script>
701
export default {
702
data() {
703
return {
704
selectedTeamMember: null,
705
teamMembers: [
706
{
707
name: 'Alice Johnson',
708
role: 'Frontend Developer',
709
avatar: 'https://via.placeholder.com/40',
710
online: true
711
},
712
{
713
name: 'Bob Smith',
714
role: 'Backend Developer',
715
avatar: 'https://via.placeholder.com/40',
716
online: false
717
},
718
{
719
name: 'Carol Williams',
720
role: 'UI/UX Designer',
721
avatar: 'https://via.placeholder.com/40',
722
online: true
723
}
724
],
725
customComponents: {
726
// Could include custom Deselect, OpenIndicator, etc.
727
}
728
};
729
}
730
};
731
</script>
732
733
<style scoped>
734
.team-select {
735
max-width: 400px;
736
}
737
738
.select-header {
739
padding: 10px 15px;
740
background: #f8f9fa;
741
border-bottom: 1px solid #e9ecef;
742
}
743
744
.select-header h4 {
745
margin: 0;
746
color: #495057;
747
}
748
749
.selected-member {
750
display: flex;
751
align-items: center;
752
gap: 8px;
753
}
754
755
.selected-member img {
756
width: 24px;
757
height: 24px;
758
border-radius: 50%;
759
}
760
761
.selected-member .role {
762
font-size: 0.8em;
763
color: #6c757d;
764
margin-left: auto;
765
}
766
767
.member-option {
768
display: flex;
769
align-items: center;
770
gap: 12px;
771
padding: 8px 0;
772
}
773
774
.member-option.offline {
775
opacity: 0.6;
776
}
777
778
.member-option img {
779
width: 40px;
780
height: 40px;
781
border-radius: 50%;
782
}
783
784
.member-details {
785
flex: 1;
786
}
787
788
.member-name {
789
font-weight: bold;
790
color: #212529;
791
}
792
793
.member-role {
794
font-size: 0.9em;
795
color: #6c757d;
796
}
797
798
.member-status {
799
display: flex;
800
align-items: center;
801
gap: 4px;
802
font-size: 0.8em;
803
color: #6c757d;
804
}
805
806
.status-dot {
807
width: 8px;
808
height: 8px;
809
border-radius: 50%;
810
background-color: #dc3545;
811
}
812
813
.status-dot.online {
814
background-color: #28a745;
815
}
816
817
.no-members {
818
padding: 20px;
819
text-align: center;
820
color: #6c757d;
821
}
822
823
.select-footer {
824
padding: 8px 15px;
825
background: #f8f9fa;
826
border-top: 1px solid #e9ecef;
827
text-align: center;
828
}
829
</style>
830
```