or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

ajax-loading.mdcustomization.mdindex.mdkeyboard-navigation.mdsearch-filtering.mdselection.mdtagging.md

customization.mddocs/

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

```