Skip to content

Repository files navigation

SnapSelect

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.

Examples

Multiple select

image

Non multiple select

image

Features

  • 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:onItemAdd and onItemDelete hooks 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.

Installation

Include the SnapSelect JavaScript and CSS files in your project:

<linkrel="stylesheet" href="/path/to/snapselect/dist/css/snapselect.min.css"><scriptsrc="/path/to/snapselect/dist/js/snapselect.min.js"></script>

Usage

Initialize SnapSelect on your desired <select> elements using any selector:

<h3>Select without optgroups</h3><!-- Normal Select --><labelfor="selectNormal">Normal Select:</label><selectid="selectNormal" name="selectNormal" class="snapSelect"><optionvalue="AR" data-key="asado Buenos Aires Spanish">Argentina</option><optionvalue="AU" data-key="vegimite Canberra English">Australia</option><optionvalue="BR" data-key="feijoada Brasília Portuguese">Brazil</option><optionvalue="CN" data-key="dumplings Beijing Mandarin">China</option><optionvalue="FR" data-key="croissant Paris French">France</option><optionvalue="IN" data-key="curry New Delhi Hindi">India</option><optionvalue="JP" data-key="sushi Tokyo Japanese">Japan</option><optionvalue="MX" data-key="tacos Mexico City Spanish">Mexico</option><optionvalue="NG" data-key="jollof Abuja English">Nigeria</option><optionvalue="RU" data-key="borscht Moscow Russian">Russia</option><optionvalue="ZA" data-key="braai Pretoria English">South Africa</option><optionvalue="ES" data-key="paella Madrid Spanish">Spain</option><optionvalue="GB" data-key="fish and chips London English">United Kingdom</option><optionvalue="US" data-key="hamburger Washington D.C. English">United States</option></select><!-- Multiple Select --><labelfor="selectMultiple">Multiple Select:</label><selectid="selectMultiple" name="selectMultiple" multipleclass="snapSelect"><optionvalue="AR" data-key="asado Buenos Aires Spanish">Argentina</option><optionvalue="AU" data-key="vegimite Canberra English">Australia</option><optionvalue="BR" data-key="feijoada Brasília Portuguese">Brazil</option><optionvalue="CN" data-key="dumplings Beijing Mandarin">China</option><optionvalue="FR" data-key="croissant Paris French">France</option><optionvalue="IN" data-key="curry New Delhi Hindi">India</option><optionvalue="JP" data-key="sushi Tokyo Japanese">Japan</option><optionvalue="MX" data-key="tacos Mexico City Spanish">Mexico</option><optionvalue="NG" data-key="jollof Abuja English">Nigeria</option><optionvalue="RU" data-key="borscht Moscow Russian">Russia</option><optionvalue="ZA" data-key="braai Pretoria English">South Africa</option><optionvalue="ES" data-key="paella Madrid Spanish">Spain</option><optionvalue="GB" data-key="fish and chips London English">United Kingdom</option><optionvalue="US" data-key="hamburger Washington D.C. English">United States</option></select><h3>Select with optgroups</h3><!-- Select with Optgroups --><labelfor="selectOptgroupNormal">Normal Select with Optgroups:</label><selectid="selectOptgroupNormal" name="selectOptgroupNormal" class="snapSelect"><optgrouplabel="Africa"><optionvalue="ZA" data-key="braai Pretoria English">South Africa</option><optionvalue="NG" data-key="jollof Abuja English">Nigeria</option><optionvalue="EG" data-key="koshari Cairo Arabic">Egypt</option></optgroup><optgrouplabel="Asia"><optionvalue="CN" data-key="dumplings Beijing Mandarin">China</option><optionvalue="IN" data-key="curry New Delhi Hindi">India</option><optionvalue="JP" data-key="sushi Tokyo Japanese">Japan</option></optgroup><optgrouplabel="Europe"><optionvalue="FR" data-key="croissant Paris French">France</option><optionvalue="ES" data-key="paella Madrid Spanish">Spain</option><optionvalue="GB" data-key="fish and chips London English">United Kingdom</option></optgroup><optgrouplabel="North America"><optionvalue="US" data-key="hamburger Washington D.C. English">United States</option><optionvalue="MX" data-key="tacos Mexico City Spanish">Mexico</option><optionvalue="CA" data-key="poutine Ottawa English">Canada</option></optgroup><optgrouplabel="South America"><optionvalue="BR" data-key="feijoada Brasília Portuguese">Brazil</option><optionvalue="AR" data-key="asado Buenos Aires Spanish">Argentina</option><optionvalue="CO" data-key="arepa Bogotá Spanish">Colombia</option></optgroup><optgrouplabel="Oceania"><optionvalue="AU" data-key="vegimite Canberra English">Australia</option><optionvalue="NZ" data-key="hangi Wellington English">New Zealand</option><optionvalue="FJ" data-key="kokoda Suva English">Fiji</option></optgroup></select><!-- Multiple Select with Optgroups --><labelfor="selectOptgroupMultiple">Multiple Select with Optgroups and data attributes:</label><selectid="selectOptgroupMultiple" name="selectOptgroupMultiple" multipleclass="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"><optgrouplabel="Africa"><optionvalue="ZA" data-key="braai Pretoria English">South Africa</option><optionvalue="NG" data-key="jollof Abuja English">Nigeria</option><optionvalue="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>

AJAX — basic remote data

Use the ajax option to load options from a remote endpoint. The select element can be left empty; SnapSelect will populate it on open.

<selectid="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", "disabled": "disabled" },
{ "id": 3, "text": "Jane Doe" }
],
"hasMore": true
}

Any properties beyond id, text and disabled are stored as data-* attributes on the underlying <option>
If disabled is set, the option will be set to disabled regardless of the value given.
A result entry may also be an optgroup: { text: 'Label', children: [{id, text, ...extras}], ...groupExtras }. Group extras beyond text/children are stored as data-* attributes on the underlying <optgroup> element (accessible via option.parentElement.dataset); disabled disables the whole group and all of its options. Child entries follow the exact same rules as top-level options.

AJAX — using processResults to map remote data

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

<selectid="selectAjax" data-placeholder="Search users..."></select><script>SnapSelect('#selectAjax',{liveSearch: true,ajax: {url: '/api/users',processResults: function(data,search,page){constrecords=Array.isArray(data)
? data
: (data.Records||[]);consttotal=data.TotalRecordCount!==undefined
? data.TotalRecordCount
: records.length;lethasMore=total>page*pagesize;// the next if covers the case where all rows are returned in 1 go, not respecting pagingif(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 (with an optgroup example):

{
"data": [
{ id: 'us', text: 'United States' },
{ text: 'Europe', countryCode: 'eu', children: [
{ id: 'be', text: 'Belgium', capital: 'Brussels' },
{ id: 'fr', text: 'France', disabled: true },
]},
{ text: 'Locked', disabled: true, children: [
{ id: 'x1', text: 'X1' },
]},
],
"TotalRecordCount": 10
}

Any properties beyond id, text and disabled are stored as data-* attributes on the underlying <option> If disabled, the option will be set to disabled regardless of the value given.
A result entry may also be an optgroup: { text: 'Label', children: [{id, text, ...extras}], ...groupExtras }. Group extras beyond text/children are stored as data-* attributes on the underlying <optgroup> element (accessible via option.parentElement.dataset); disabled disables the whole group and all of its options. Child entries follow the exact same rules as top-level options.

AJAX — with extra parameters and POST

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:

<selectid="selectAjaxPost" data-placeholder="Search products..."></select><script>// Plain object — same extra params on every requestSnapSelect('#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/pageSnapSelect('#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>

AJAX — dynamic URL (country → state cascade)

The url option accepts a function, making dependent dropdowns straightforward:

<selectid="country" data-placeholder="Select country..."></select><selectid="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(){conststateEl=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>

AJAX — custom headers and minimum input length

SnapSelect('#selectAjax',{ajax: {url: '/api/search',minimumInputLength: 2,// only fetch after 2 characters are typeddelay: 400,// debounce delay in msheaders: {'Authorization': 'Bearer my-token','X-Custom-Header': 'value'},processResults: function(data,search,page){return{results: data.results,hasMore: false};}}});

Events

The change-event and input-event on the underlying <select> are still emitted, so you can hook into those as usual.

Supported Data Attributes

  • 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 for data-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.
  • data-open-on-focus="true|false" (boolean): If true, opens the dropdown after focus.

Configuration Options

Core options

  • 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: ``. This causes an empty option to be added at the top if not present.
  • defaultText (string): the text shown on the select box if the first option value and string are empty (placeholder takes precedence if present). 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: true if placeholder is set, false otherwise. 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.
  • openOnFocus (boolean): Opens the dropdown automatically when the select receives focus (e.g. via Tab). Default: false.
  • openOnFocusIfEmoty (boolean): Opens the dropdown automatically when the select receives focus (e.g. via Tab) and nothing is selected yet. Default: false.

Callbacks

  • onItemAdd (function): Called whenever an item is selected. Receives (value, text) — the option's value attribute and its visible label text. Fired for both single and multi-select. Inside the callback, this refers 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, this refers to the underlying <select> element.
SnapSelect('#mySelect',{onItemAdd: function(value,text){// `this` is the <select> elementconstform=this.closest('form');console.log('Added:',value,text);},onItemDelete: function(value,text){// `this` is the <select> elementconstform=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 options

  • 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) => string for dynamic URLs. When a function, this refers 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, this refers 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: [...], hasMore: bool }. this refers to the underlying <select> element. If omitted, SnapSelect expects the response to be either a plain array or an object with a results array and a hasMore boolean. Each result entry is one of:
      • an option: { id, text, disabled?, ...extras }. Any extra properties beyond id, text and disabled are stored as data-* attributes on the underlying <option> element, making them accessible in onItemAdd/onItemDelete via this.querySelector(\option[value="${value}"]`).dataset`.
      • an optgroup: { text: 'Label', children: [{id, text, ...}], disabled?, ...extras }. The group label comes from text (or label); disabled disables the whole group and all of its options. Group extras beyond text, label, children and disabled are stored as data-* attributes on the underlying <optgroup> element, accessible via option.parentElement.dataset. Child entries follow the exact same rules as top-level options.
    • 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: false if url or data is a function, true otherwise.
    • 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'.

Public Methods

After initialisation, SnapSelect returns an instance with the following methods:

constselect=SnapSelect('#mySelect',{/* options */});// Clear the current selectionselect.clear();// Force a fresh AJAX fetch (clears cache for the current search term)select.refresh();// Clear the entire AJAX response cacheselect.clearCache();// Sets and refreshes the placeholderselect.setPlaceholder('my new placeholder text');

Form Validation

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.

<selectid="selectRequired" name="category" requireddata-placeholder="Select a category..."><optionvalue=""></option><optionvalue="1">Option A</option><optionvalue="2">Option B</option></select>

The validation message text is sourced from the browser's own validationMessage, so it is automatically localised.

Accessibility (ARIA)

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" with aria-expanded and aria-haspopup attributes.
  • The items container is set as role="listbox".
  • Selected items are dynamically updated with aria-live="polite" for screen reader notifications.

Contributing

Contributions are welcome! Please fork the repository and submit a pull request for any features, bug fixes, or improvements.

License

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.

About

SnapSelect is a JavaScript plugin that is designed to improve the functionality of HTML <select> elements. It is lightweight, customizable, and simple to use. SnapSelect offers a modern and sleek interface with advanced features, while remaining highly configurable and efficient.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages