or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-configuration.mdbasic-selection.mdcustom-rendering.mdgrouped-options.mdindex.mdsearch-filtering.mdtagging-mode.md

custom-rendering.mddocs/

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

```