Tom Select
Enhanced searchable select with tagging, remote search, option groups, and custom templates — powered by Tom Select v2.
Tom Select is initialised by calling new TomSelect('#id', options) inside a DOMContentLoaded listener (or after AJAX navigation). Every instance below uses a self-contained <script> block so examples are copy-pasteable. In a real project, consolidate initialisations into one file under /js/.
Single Select — Searchable
Replaces a native <select> with a searchable dropdown. Tom Select adds keyboard navigation, type-ahead filtering, and a clear button. Use allowEmptyOption: true when the placeholder option needs to be selectable.
Basic
With clear button
Disabled
<select id="my-select">
<option value="">Select a country…</option>
<option value="id">Indonesia</option>
<option value="sg">Singapore</option>
</select>
<script>
document.addEventListener('DOMContentLoaded', () => {
new TomSelect('#my-select', {
allowEmptyOption: true,
plugins: ['clear_button'], // optional ×
});
});
</script>
Multi-Select with Tags
Each selected option renders as a removable tag (pill) inside the control. The remove_button plugin adds the × per tag. Combine with max_items to cap the number of selections.
Product categories (unlimited)
Tags — max 3 selections
Maximum 3 tags. Further options are hidden once limit is reached.
<select id="my-multi" multiple placeholder="Add tags…">
<option value="sale" selected>On Sale</option>
<option value="new">New Arrival</option>
</select>
<script>
new TomSelect('#my-multi', {
plugins: ['remove_button'],
maxItems: 3, // omit for unlimited
placeholder: 'Add tags…',
onItemAdd() {
this.setTextboxValue('');
this.refreshOptions();
},
});
</script>
Create New Option
When create: true, typing a value not in the list shows an "Add …" prompt. Pressing Enter or clicking it adds the option on the fly. Useful for tag inputs where the option set is user-defined — e.g. product labels, skills, ingredients.
Free-form tags (type anything + Enter)
Single with create (e.g. city field)
new TomSelect('#my-select', {
create: true, // show "Add …" prompt for unknown input
createOnBlur: false, // only create on explicit Enter / click
plugins: ['remove_button'],
render: {
option_create(data, escape) {
return `<div class="create">Add <strong>${escape(data.input)}</strong>…</div>`;
},
},
});
Option Groups
Native <optgroup> elements are preserved by Tom Select. Group headers are styled with the project's overline treatment. Use when the option list spans multiple logical categories — shipping methods, role types, time zones.
Shipping method
Time zone
<select id="shipping">
<option value="">Choose shipping…</option>
<optgroup label="Standard">
<option value="reg">Regular (5–7 days)</option>
<option value="eco">Economy (10–14 days)</option>
</optgroup>
<optgroup label="Express">
<option value="next">Next Day</option>
<option value="same">Same Day</option>
</optgroup>
</select>
<script>
new TomSelect('#shipping', {
allowEmptyOption: true,
plugins: ['clear_button'],
});
</script>
Disabled Options
Options marked disabled on the native <option> are shown in the list but cannot be selected. Use this to signal unavailability (out of stock, plan restriction, coming soon) without removing the option from context.
Plan upgrade (some locked)
Delivery slot (some taken)
<!-- Mark individual options as disabled on the native element -->
<select id="my-select">
<option value="free">Free — 5 products</option>
<option value="business" disabled>Business ✦ Contact sales</option>
</select>
<!-- Tom Select inherits the disabled state automatically -->
<script>
new TomSelect('#my-select', {});
</script>
Custom Option Template
The render.option and render.item callbacks return HTML strings, enabling rich option display — avatars, flags, subtitles, badges. The valueField / labelField / searchField options control which object keys Tom Select reads for value, display, and filtering.
Assignee (avatar + name)
Country (flag emoji)
new TomSelect('#assignee', {
options: [
{ id: '1', name: 'Arya Wijaya', role: 'Frontend', initials: 'AW' },
{ id: '2', name: 'Budi Santoso', role: 'Backend', initials: 'BS' },
],
valueField: 'id',
labelField: 'name',
searchField: ['name', 'role'],
placeholder: 'Assign to…',
plugins: ['clear_button'],
render: {
option(data, escape) {
return `<div class="flex items-center gap-2">
<span class="ts-opt-avatar">${escape(data.initials)}</span>
<span>
<span class="block">${escape(data.name)}</span>
<span class="ts-opt-meta">${escape(data.role)}</span>
</span>
</div>`;
},
item(data, escape) {
return `<div>${escape(data.initials)} ${escape(data.name)}</div>`;
},
},
});
Remote Search (AJAX)
Options are loaded on demand via load(), which fires when the user types. A debounce prevents flooding the server on every keystroke. The example below simulates an API call with a 400 ms delay — replace the setTimeout with a real fetch. The shouldLoad callback skips the request for short queries.
Product search (type "head", "key", or "usb")
new TomSelect('#product-search', {
valueField: 'id',
labelField: 'name',
searchField: ['name', 'sku'],
placeholder: 'Search products…',
shouldLoad(q) { return q.length >= 2; }, // skip 0–1 char queries
load(query, callback) {
fetch('/api/products?q=' + encodeURIComponent(query))
.then(r => r.json())
.then(callback)
.catch(() => callback());
},
plugins: ['clear_button'],
render: {
option(data, escape) {
return `<div class="flex items-center justify-between gap-3">
<span>
<span class="block">${escape(data.name)}</span>
<span class="ts-opt-meta">${escape(data.sku)}</span>
</span>
<span class="font-medium text-sm">${escape(data.price)}</span>
</div>`;
},
loading: () => '<div class="no-results">Searching…</div>',
no_results: (d, escape) => `<div class="no-results">No results for "${escape(d.input)}"</div>`,
},
});
Inside a Form — Error & Helper States
Tom Select wraps the original <select> in a .ts-wrapper div. Error state is signalled by adding ts-error on the wrapper after init. A helper / error message sits below as a sibling element — not inside the wrapper.
<fieldset>
<legend class="fieldset-legend">Shipping method <span class="text-error">*</span></legend>
<select id="shipping-method">
<option value="">Select method…</option>
<option value="reg">Regular</option>
<option value="exp">Express</option>
</select>
<p class="fieldset-label text-error mt-1">Please select a shipping method.</p>
</fieldset>
<script>
// Apply error styling after init via the returned instance
const ts = new TomSelect('#shipping-method', { allowEmptyOption: true });
ts.control.style.borderColor = 'oklch(var(--er))';
</script>