SnapSelect is a lightweight, customizable, and easy-to-use JavaScript plugin designed to enhance the functionality of HTML <select> elements. Inspired by popular select plugins like Select2, TomSelect, and Slim Select, SnapSelect offers a modern and sleek interface with advanced features, while remaining highly configurable and performant.
- Live Search: Enable users to quickly filter options as they type.
- Ajax call: Remote data with infinite scrolling.
- Multi-Select Support: Allows for multiple selections with intuitive tag management.
- Optgroup Selection: Easily select or deselect all options within an optgroup.
- Clear Button: Option to show a clear button to reset the current selection (works for both single and multi-select).
- Select All Option: Add a "Select All" option to quickly select all available options (for multi-select).
- Customizable Placeholder: Define custom placeholder text for an empty selection.
- Custom Search Keywords: Enhance search functionality with custom keywords for each option.
- Bootstrap-Inspired Design: Styled to seamlessly integrate with Bootstrap 5, but easily customizable to match any design system.
- Callbacks:
onItemAddandonItemDeletehooks for reacting to individual selection changes. - Accessible: Built with accessibility in mind, ensuring a better experience for all users.
- Lightweight: Minimal footprint, optimized for performance.
Include the SnapSelect JavaScript and CSS files in your project:
<link rel="stylesheet" href="/path/to/snapselect/dist/css/snapselect.min.css">
<script src="/path/to/snapselect/dist/js/snapselect.min.js"></script>Initialize SnapSelect on your desired <select> elements using any selector:
<h3>Select without optgroups</h3>
<!-- Normal Select -->
<label for="selectNormal">Normal Select:</label>
<select id="selectNormal" name="selectNormal" class="snapSelect">
<option value="AR" data-key="asado Buenos Aires Spanish">Argentina</option>
<option value="AU" data-key="vegimite Canberra English">Australia</option>
<option value="BR" data-key="feijoada Brasília Portuguese">Brazil</option>
<option value="CN" data-key="dumplings Beijing Mandarin">China</option>
<option value="FR" data-key="croissant Paris French">France</option>
<option value="IN" data-key="curry New Delhi Hindi">India</option>
<option value="JP" data-key="sushi Tokyo Japanese">Japan</option>
<option value="MX" data-key="tacos Mexico City Spanish">Mexico</option>
<option value="NG" data-key="jollof Abuja English">Nigeria</option>
<option value="RU" data-key="borscht Moscow Russian">Russia</option>
<option value="ZA" data-key="braai Pretoria English">South Africa</option>
<option value="ES" data-key="paella Madrid Spanish">Spain</option>
<option value="GB" data-key="fish and chips London English">United Kingdom</option>
<option value="US" data-key="hamburger Washington D.C. English">United States</option>
</select>
<!-- Multiple Select -->
<label for="selectMultiple">Multiple Select:</label>
<select id="selectMultiple" name="selectMultiple" multiple class="snapSelect">
<option value="AR" data-key="asado Buenos Aires Spanish">Argentina</option>
<option value="AU" data-key="vegimite Canberra English">Australia</option>
<option value="BR" data-key="feijoada Brasília Portuguese">Brazil</option>
<option value="CN" data-key="dumplings Beijing Mandarin">China</option>
<option value="FR" data-key="croissant Paris French">France</option>
<option value="IN" data-key="curry New Delhi Hindi">India</option>
<option value="JP" data-key="sushi Tokyo Japanese">Japan</option>
<option value="MX" data-key="tacos Mexico City Spanish">Mexico</option>
<option value="NG" data-key="jollof Abuja English">Nigeria</option>
<option value="RU" data-key="borscht Moscow Russian">Russia</option>
<option value="ZA" data-key="braai Pretoria English">South Africa</option>
<option value="ES" data-key="paella Madrid Spanish">Spain</option>
<option value="GB" data-key="fish and chips London English">United Kingdom</option>
<option value="US" data-key="hamburger Washington D.C. English">United States</option>
</select>
<h3>Select with optgroups</h3>
<!-- Select with Optgroups -->
<label for="selectOptgroupNormal">Normal Select with Optgroups:</label>
<select id="selectOptgroupNormal" name="selectOptgroupNormal" class="snapSelect">
<optgroup label="Africa">
<option value="ZA" data-key="braai Pretoria English">South Africa</option>
<option value="NG" data-key="jollof Abuja English">Nigeria</option>
<option value="EG" data-key="koshari Cairo Arabic">Egypt</option>
</optgroup>
<optgroup label="Asia">
<option value="CN" data-key="dumplings Beijing Mandarin">China</option>
<option value="IN" data-key="curry New Delhi Hindi">India</option>
<option value="JP" data-key="sushi Tokyo Japanese">Japan</option>
</optgroup>
<optgroup label="Europe">
<option value="FR" data-key="croissant Paris French">France</option>
<option value="ES" data-key="paella Madrid Spanish">Spain</option>
<option value="GB" data-key="fish and chips London English">United Kingdom</option>
</optgroup>
<optgroup label="North America">
<option value="US" data-key="hamburger Washington D.C. English">United States</option>
<option value="MX" data-key="tacos Mexico City Spanish">Mexico</option>
<option value="CA" data-key="poutine Ottawa English">Canada</option>
</optgroup>
<optgroup label="South America">
<option value="BR" data-key="feijoada Brasília Portuguese">Brazil</option>
<option value="AR" data-key="asado Buenos Aires Spanish">Argentina</option>
<option value="CO" data-key="arepa Bogotá Spanish">Colombia</option>
</optgroup>
<optgroup label="Oceania">
<option value="AU" data-key="vegimite Canberra English">Australia</option>
<option value="NZ" data-key="hangi Wellington English">New Zealand</option>
<option value="FJ" data-key="kokoda Suva English">Fiji</option>
</optgroup>
</select>
<!-- Multiple Select with Optgroups -->
<label for="selectOptgroupMultiple">Multiple Select with Optgroups and data attributes:</label>
<select id="selectOptgroupMultiple" name="selectOptgroupMultiple" multiple class="snapSelect"
data-live-search="true"
data-placeholder="Choose an option"
data-max-selections="3"
data-show-clear-button="true"
data-select-optgroups="true"
data-select-all-option="true"
data-close-on-select="false">
<optgroup label="Africa">
<option value="ZA" data-key="braai Pretoria English">South Africa</option>
<option value="NG" data-key="jollof Abuja English">Nigeria</option>
<option value="EG" data-key="koshari Cairo Arabic">Egypt</option>
</optgroup>
<!-- ... -->
</select>
<script>
document.addEventListener('DOMContentLoaded', () => {
SnapSelect('.snapSelect', {
liveSearch: true,
placeholder: 'Select an option...',
showClearButton: true,
selectOptgroups: true,
selectAllOption: true,
closeOnSelect: false,
maxSelections: 10,
});
});
</script>Use the ajax option to load options from a remote endpoint. The select element can be left empty; SnapSelect will populate it on open.
<select id="selectAjax" data-placeholder="Search users..."></select>
<script>
SnapSelect('#selectAjax', {
liveSearch: true,
ajax: {
url: '/api/users',
}
});
</script>The server receives q (search term), page, and pagesize as query parameters on every request:
GET /api/users?q=john&page=1&pagesize=20
Expected response shape:
{
"data": [
{ "id": 1, "text": "John Doe" },
{ "id": 2, "text": "John Smith" }
],
"hasMore": true
}Use the processResults option to parse the ajax answer and convert it to the expected format. In this example we get TotalRecordCount from the backend, indicating the total number of records
<select id="selectAjax" data-placeholder="Search users..."></select>
<script>
SnapSelect('#selectAjax', {
liveSearch: true,
ajax: {
url: '/api/users',
processResults: function(data, search, page) {
const records = Array.isArray(data)
? data
: (data.Records || []);
const total = data.TotalRecordCount !== undefined
? data.TotalRecordCount
: records.length;
let hasMore = total > page * pagesize;
// the next if covers the case where all rows are returned in 1 go, not respecting paging
if (data.TotalRecordCount !== undefined && records.length >= data.TotalRecordCount)
hasMore = false;
return { results: records, hasMore };
}
}
});
</script>The server receives q (search term), page, and pagesize as query parameters on every request:
GET /api/users?q=john&page=1&pagesize=2
Example response shape:
{
"data": [
{ "id": 1, "text": "John Doe" },
{ "id": 2, "text": "John Smith" }
],
"TotalRecordCount": 10
}Use data to pass extra parameters (as a plain object or a function), and method to switch to POST. For POST requests, parameters are sent as multipart/form-data, which is compatible with traditional server frameworks like WordPress:
<select id="selectAjaxPost" data-placeholder="Search products..."></select>
<script>
// Plain object — same extra params on every request
SnapSelect('#selectAjaxPost', {
ajax: {
url: '/api/products/search',
method: 'POST',
data: {
category: 'electronics',
inStock: true
},
processResults: function(response) {
return {
results: response.items,
hasMore: response.hasNextPage
};
}
}
});
// Function — extra params can vary per search/page
SnapSelect('#selectAjaxPost', {
ajax: {
url: '/api/products/search',
method: 'POST',
data: function(searchTerm, page) {
return {
category: document.getElementById('categoryFilter').value
};
},
processResults: function(data, search, page) {
return {
results: data.items,
hasMore: data.hasNextPage
};
}
}
});
</script>The url option accepts a function, making dependent dropdowns straightforward:
<select id="country" data-placeholder="Select country..."></select>
<select id="state" data-placeholder="Select state..." disabled></select>
<script>
SnapSelect('#country', {
ajax: {
url: '/api/countries',
processResults: r => ({ results: r.data, hasMore: r.meta.hasMore })
}
});
document.getElementById('country').addEventListener('change', function() {
const stateEl = document.getElementById('state');
stateEl.disabled = !this.value;
SnapSelect('#state', {
ajax: {
url: (search, page) => `/api/states?country=${this.value}&q=${search}&page=${page}`,
processResults: r => ({ results: r.data, hasMore: r.meta.hasMore })
}
});
});
</script>SnapSelect('#selectAjax', {
ajax: {
url: '/api/search',
minimumInputLength: 2, // only fetch after 2 characters are typed
delay: 400, // debounce delay in ms
headers: {
'Authorization': 'Bearer my-token',
'X-Custom-Header': 'value'
},
processResults: function(data, search, page) {
return { results: data.results, hasMore: false };
}
}
});The change-event and input-event on the underlying <select> are still emitted, so you can hook into those as usual.
data-live-search="true|false"(boolean): Enables or disables the search functionality.data-max-selections="number"(number): Sets the maximum number of selections allowed.data-max-items="number"(number): Alias fordata-max-selections.data-placeholder="text"(string): Sets the placeholder text when no option is selected.data-show-clear-button="true|false"(boolean): Shows or hides the clear button (works for both single and multi-select). Deprecated aliases:data-clear-all-button,data-allow-empty.data-select-optgroups="true|false"(boolean): Allows or disallows selecting all options within an optgroup.data-select-all-option="true|false"(boolean): Adds or removes the "Select All" option.data-close-on-select="true|false"(boolean): Closes or keeps open the dropdown after selection.
liveSearch(boolean): Enable live search functionality. Default:false.maxSelections(number): Maximum number of selections allowed (for multi-select). Alias:maxItems. Default:Infinity.placeholder(string): Custom placeholder text. Default:'Select...'.showClearButton(boolean): Show a button to clear the current selection. For multi-select, clears all tags at once; for single-select, reverts to the placeholder. Default:false. Deprecated aliases:clearAllButton,allowEmpty.selectOptgroups(boolean): Allow selecting/deselecting all options within an optgroup. Default:false.selectAllOption(boolean): Add a "Select All" option for multi-select. Default:false.closeOnSelect(boolean): Close the dropdown after each selection (single-select only). Default:true.
onItemAdd(function): Called whenever an item is selected. Receives(value, text)— the option'svalueattribute and its visible label text. Fired for both single and multi-select. Inside the callback,thisrefers to the underlying<select>element.onItemDelete(function): Called whenever an item is deselected. Receives(value, text). In multi-select mode this fires per individual item removed (whether via the tag × button or the dropdown checkbox). In single-select mode it also fires for the previously selected value when it is replaced by a new selection. Inside the callback,thisrefers to the underlying<select>element.
SnapSelect('#mySelect', {
onItemAdd: function(value, text) {
// `this` is the <select> element
const form = this.closest('form');
console.log('Added:', value, text);
},
onItemDelete: function(value, text) {
// `this` is the <select> element
const form = this.closest('form');
console.log('Removed:', value, text);
}
});Both callbacks work in static and AJAX modes and are independent of the native change event — each receives exactly the one value that changed, making them more precise than listening to change directly on the underlying <select>.
ajax(object): Enables remote data loading. When set, the dropdown fetches options from a server instead of reading them from the HTML. Default:null.url(string|function) required: The endpoint URL, or a function(searchTerm, page) => stringfor dynamic URLs. When a function,thisrefers to the underlying<select>element. When a function, this also changes the default cache value (see that option).method(string): HTTP method. Default:'GET'.data(object|function): Optional. A plain object of extra parameters, or a function(searchTerm, page) => object, merged into every request. When a function,thisrefers to the underlying<select>element. When a function, this also changes the default cache value (see that option).processResults(function): Optional. Maps the raw server response to{ results: [{id, text, ...extras}], hasMore: bool }.thisrefers to the underlying<select>element. If omitted, SnapSelect expects the response to be either a plain array or an object with aresultsarray and ahasMoreboolean. Any extra properties on each result item beyondidandtextare stored asdata-*attributes on the underlying<option>element, making them accessible inonItemAdd/onItemDeleteviathis.querySelector(\option[value="${value}"]`).dataset`.delay(number): Debounce delay in ms before firing a search request. Default:300.minimumInputLength(number): Minimum number of characters required before fetching. Default:0.cache(boolean): Cache results per (search + page) key to avoid redundant requests. Default:falseifurlordatais a function,trueotherwise.headers(object): Extra HTTP headers to include in every request. Default:{}.pagesize(number): Number of items per page sent to the server. Default:20.loadingText(string): Text shown in the dropdown while loading. Default:'Loading...'.noResultsText(string): Text shown when no results are returned. Default:'No results found'.errorText(string): Text shown when the request fails. Default:'Error loading results'.
After initialisation, SnapSelect returns an instance with the following methods:
const select = SnapSelect('#mySelect', { /* options */ });
// Clear the current selection
select.clear();
// Force a fresh AJAX fetch (clears cache for the current search term)
select.refresh();
// Clear the entire AJAX response cache
select.clearCache();SnapSelect respects the native required attribute. When a required field is submitted empty, the widget highlights the select with a snap-select-invalid class on the .snap-select-selected element and renders a validation message below the widget using .snap-select-validation-message. Both classes can be styled freely in your CSS.
<select id="selectRequired" name="category" required data-placeholder="Select a category...">
<option value=""></option>
<option value="1">Option A</option>
<option value="2">Option B</option>
</select>The validation message text is sourced from the browser's own validationMessage, so it is automatically localised.
SnapSelect is designed with accessibility in mind and includes ARIA attributes to enhance usability with assistive technologies.
- The main container is set as
role="combobox"witharia-expandedandaria-haspopupattributes. - The items container is set as
role="listbox". - Selected items are dynamically updated with
aria-live="polite"for screen reader notifications.
Contributions are welcome! Please fork the repository and submit a pull request for any features, bug fixes, or improvements.
SnapSelect is released under the MIT License. See the LICENSE file for more details. Feel free to use in any condition and modify all as you want.

