@smilodon/vanilla
v1.9.2
Published
Vanilla JavaScript helpers for Smilodon Select - part of the Smilodon UI toolkit
Maintainers
Readme
@smilodon/vanilla
Vanilla JavaScript/TypeScript helpers for Smilodon Select. Part of the Smilodon UI toolkit.
Features
- ✨ Simple API - Easy-to-use helper functions for vanilla JS
- 🔍 Zero Dependencies - Only requires @smilodon/core
- ♿ Fully Accessible - WCAG 2.1 AAA compliant
- ⚡ Tiny Bundle - < 1KB gzipped
- 🔧 TypeScript - Full type safety included
- 📦 Tree-Shakeable - Import only what you need
- 🎯 Web Standards - Built on native Web Components
📖 Documentation
Live documentation & interactive examples: navidrezadoost.github.io/Smilodon
Framework integration (Vue 3, Nuxt 4, React, SSR, Vite): docs/FRAMEWORK-INTEGRATION.md
Installation
npm install @smilodon/vanilla @smilodon/coreQuick Start
Using the Helper Function
import { createSelect } from '@smilodon/vanilla';
const select = createSelect({
items: [
{ value: '1', label: 'Apple' },
{ value: '2', label: 'Banana' },
{ value: '3', label: 'Cherry' },
],
placeholder: 'Select a fruit...',
onChange: (value, items) => {
console.log('Selected:', value, items);
}
});
document.body.appendChild(select);Direction and option-state controls
const select = createSelect({
items,
direction: 'rtl',
removeButtonIcon: '−',
disabledOptionBehavior: {
hoverable: true,
focusable: true,
selectable: false,
},
showSelectedIndicator: false,
});directionswitches the instance betweenltrandrtlremoveButtonIconreplaces the chip remove icon with text or inline SVG markupdisabledOptionBehaviorcontrols whether dimmed options can still be hovered, focused, or selectedshowSelectedIndicatorhides or shows the selected side indicator without custom pseudo-element overrides
Core config parity and global defaults
The Vanilla helpers now expose both adapter-style convenience options and the full shared core config shape.
Convenience options on createSelect() / initSelect()
Use these when you want advanced core features without writing the whole updateConfig() object yourself:
selectionConfigmultiSelectDisplayscrollToSelectedstylesconfig
import { createSelect } from '@smilodon/vanilla';
const select = createSelect({
items: [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
{ value: 'svelte', label: 'Svelte' },
{ value: 'solid', label: 'Solid' },
],
value: ['react', 'solid'],
multiple: true,
searchable: true,
multiSelectDisplay: {
mode: 'horizontal',
maxHeight: '56px',
overflowX: 'auto',
overflowY: 'hidden',
dragScroll: true,
},
selectionConfig: {
closeOnSelect: false,
toggleOnTriggerClick: true,
},
styles: {
badgeRemoveIcon: {
color: '#dc2626',
transform: 'scale(1.1)',
},
},
removeButtonIcon: '−',
});Full config passthrough
const select = createSelect({
items,
multiple: true,
config: {
dropdownPlacement: { mode: 'auto' },
scrollToSelected: {
enabled: true,
multiSelectTarget: 'last',
},
multiSelectDisplay: {
mode: 'vertical',
maxHeight: '120px',
},
selection: {
allowDeselect: true,
closeOnSelect: false,
},
styles: {
badge: {
background: '#eff6ff',
border: '1px solid #bfdbfe',
color: '#1d4ed8',
},
badgeRemoveIcon: {
color: '#1d4ed8',
},
},
},
});Global defaults from @smilodon/vanilla
import { configureSelect, createSelect } from '@smilodon/vanilla';
configureSelect({
searchable: true,
clearControl: {
enabled: true,
clearSelection: true,
clearSearch: true,
},
selection: {
showSelectedIndicator: false,
removeButtonIcon: '×',
},
});
const select = createSelect({ items });Expanded helper API
In addition to open(), close(), clear(), setItems(), and setGroupedItems(), the Vanilla helper layer now exposes:
clearSearch(element)getSelectedItems(element)updateConfig(element, config)setError(element, message)clearError(element)setRequired(element, required)validate(element)
import {
clearSearch,
updateConfig,
setError,
validate,
} from '@smilodon/vanilla';
clearSearch(select);
updateConfig(select, { selection: { showSelectedIndicator: false } });
setError(select, 'Please choose at least one option');
console.log(validate(select));Using the Web Component Directly
<!DOCTYPE html>
<html>
<head>
<script type="module">
import '@smilodon/core';
import { initSelect } from '@smilodon/vanilla';
const select = document.querySelector('enhanced-select');
initSelect(select, {
items: [
{ value: 'js', label: 'JavaScript' },
{ value: 'ts', label: 'TypeScript' },
{ value: 'py', label: 'Python' },
],
value: 'js'
});
</script>
</head>
<body>
<enhanced-select placeholder="Select a language..."></enhanced-select>
</body>
</html>Examples
Searchable Select
import { createSelect } from '@smilodon/vanilla';
const select = createSelect({
items: [
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'mx', label: 'Mexico' },
],
searchable: true,
placeholder: 'Search countries...',
onChange: (value) => console.log('Country:', value)
});
document.getElementById('app').appendChild(select);Multi-Select
import { createSelect } from '@smilodon/vanilla';
const select = createSelect({
items: [
{ value: 'js', label: 'JavaScript' },
{ value: 'ts', label: 'TypeScript' },
{ value: 'py', label: 'Python' },
],
multiple: true,
placeholder: 'Select languages...',
onChange: (values) => {
console.log('Selected languages:', values);
}
});
document.getElementById('app').appendChild(select);Grouped Options
import { createSelect } from '@smilodon/vanilla';
const select = createSelect({
groupedItems: [
{
group: 'Fruits',
items: [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
],
},
{
group: 'Vegetables',
items: [
{ value: 'carrot', label: 'Carrot' },
{ value: 'potato', label: 'Potato' },
],
},
],
onChange: (value) => console.log('Food:', value)
});
document.getElementById('app').appendChild(select);Infinite Scroll
import { createSelect, setItems } from '@smilodon/vanilla';
let items = generateItems(1, 50);
let page = 1;
const select = createSelect({
items,
infiniteScroll: true,
pageSize: 50,
placeholder: 'Scroll to load more...',
onLoadMore: (pageNum) => {
page++;
const newItems = generateItems(page * 50 + 1, (page + 1) * 50);
items = [...items, ...newItems];
setItems(select, items);
}
});
function generateItems(start, end) {
return Array.from({ length: end - start + 1 }, (_, i) => ({
value: start + i,
label: `Item ${start + i}`,
}));
}
document.getElementById('app').appendChild(select);Virtual Scrolling (Large Datasets)
import { createSelect } from '@smilodon/vanilla';
// Generate 100,000 items
const items = Array.from({ length: 100000 }, (_, i) => ({
value: i + 1,
label: `Item ${i + 1}`,
}));
const select = createSelect({
items,
virtualized: true,
searchable: true,
placeholder: '100k items with virtual scrolling...'
});
document.getElementById('app').appendChild(select);Server-Side Search
import { createSelect, setItems } from '@smilodon/vanilla';
const select = createSelect({
searchable: true,
placeholder: 'Type to search...',
onSearch: async (query) => {
if (!query) {
setItems(select, []);
return;
}
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
setItems(select, data.results);
}
});
document.getElementById('app').appendChild(select);Programmatic Control
import { createSelect, getValue, setValue, clear, open, close } from '@smilodon/vanilla';
const select = createSelect({
items: [
{ value: 'draft', label: 'Draft' },
{ value: 'published', label: 'Published' },
{ value: 'archived', label: 'Archived' },
]
});
// Add to DOM
document.getElementById('app').appendChild(select);
// Programmatic API
document.getElementById('btn-open').addEventListener('click', () => {
open(select);
});
document.getElementById('btn-close').addEventListener('click', () => {
close(select);
});
document.getElementById('btn-set').addEventListener('click', () => {
setValue(select, 'published');
});
document.getElementById('btn-get').addEventListener('click', () => {
console.log('Current value:', getValue(select));
});
document.getElementById('btn-clear').addEventListener('click', () => {
clear(select);
});Clear Control (Reset Selection/Search)
import { createSelect } from '@smilodon/vanilla';
const select = createSelect({
items: [
{ value: 'js', label: 'JavaScript' },
{ value: 'py', label: 'Python' },
],
searchable: true,
clearable: true,
clearSelectionOnClear: true,
clearSearchOnClear: true,
clearAriaLabel: 'Clear selected and searched values',
clearIcon: '✕',
onClear: (detail) => console.log('cleared', detail),
style: {
'--select-clear-button-bg': 'rgba(0, 0, 0, 0.06)',
'--select-clear-button-hover-bg': 'rgba(0, 0, 0, 0.12)',
'--select-clear-button-color': '#374151',
} as any,
});
document.body.appendChild(select);Styling hooks: ::part(clear-button), ::part(clear-icon), and --select-clear-* CSS variables.
The default hideWhenEmpty: true behavior means an empty clearable select reserves no action-area space. A selection or enabled search query reveals the button and coordinated input/arrow layout; clearing both restores the normal layout. This applies to single, multi-select, LTR, and RTL modes.
Use full config passthrough when a permanently visible disabled-when-empty action is intentional:
const select = createSelect({
items,
clearable: true,
config: { clearControl: { hideWhenEmpty: false } },
});onClear reports clearedSelection and clearedSearch. Prefer the clear-button parts and tokens over adding permanent input padding.
With Event Listeners
import { createSelect } from '@smilodon/vanilla';
const select = createSelect({
items: [...],
onChange: (value, items) => {
console.log('Value changed:', value);
console.log('Selected items:', items);
},
onSelect: (item, index) => {
console.log('Item selected:', item, 'at index:', index);
},
onOpen: () => {
console.log('Dropdown opened');
},
onClose: () => {
console.log('Dropdown closed');
},
onSearch: (query) => {
console.log('Search query:', query);
}
});
document.getElementById('app').appendChild(select);Custom Styling
import { createSelect } from '@smilodon/vanilla';
const select = createSelect({
items: [...],
className: 'my-custom-select',
style: {
width: '300px',
marginTop: '20px',
}
});
document.getElementById('app').appendChild(select);/* Custom CSS variables */
.my-custom-select {
--select-border-color: #3b82f6;
--select-focus-border-color: #1d4ed8;
--select-background: #f9fafb;
--select-text-color: #111827;
}API Reference
createSelect(options)
Creates and returns a new select element.
Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| items | SelectItem[] | [] | Array of selectable items |
| groupedItems | GroupedItem[] | undefined | Grouped items array |
| value | string \| number \| Array | undefined | Initial value |
| multiple | boolean | false | Enable multi-select |
| searchable | boolean | false | Enable search/filter |
| placeholder | string | '' | Placeholder text |
| disabled | boolean | false | Disable the select |
| required | boolean | false | Mark as required |
| error | boolean | false | Show error state |
| infiniteScroll | boolean | false | Enable infinite scroll |
| pageSize | number | 50 | Items per page |
| virtualized | boolean | true | Enable virtual scrolling |
| maxSelections | number | undefined | Max selections |
| placement | 'bottom' \| 'top' \| 'auto' | 'auto' | Dropdown placement |
| className | string | '' | Custom CSS class |
| style | CSSStyleDeclaration | {} | Inline styles |
| onChange | (value, items) => void | undefined | Change callback |
| onSelect | (item, index) => void | undefined | Select callback |
| onOpen | () => void | undefined | Open callback |
| onClose | () => void | undefined | Close callback |
| onSearch | (query) => void | undefined | Search callback |
| onLoadMore | (page) => void | undefined | Load more callback |
| onCreate | (value) => void | undefined | Create callback |
Returns: HTMLElement
initSelect(element, options)
Initialize an existing select element in the DOM.
Parameters:
element: HTMLElement- The select elementoptions: { items?, groupedItems?, value? }- Initialization options
getValue(element)
Get the selected value(s).
Returns: string | number | (string | number)[]
setValue(element, value)
Set the selected value(s).
Parameters:
element: HTMLElementvalue: string | number | (string | number)[]
clear(element)
Clear the selection.
open(element)
Open the dropdown.
close(element)
Close the dropdown.
setItems(element, items)
Update the items.
Parameters:
element: HTMLElementitems: SelectItem[]
setGroupedItems(element, groups)
Update the grouped items.
Parameters:
element: HTMLElementgroups: GroupedItem[]
Types
interface SelectItem {
value: string | number;
label: string;
disabled?: boolean;
group?: string;
[key: string]: any;
}
interface GroupedItem {
group: string;
items: SelectItem[];
}Styling
The component uses CSS variables for customization:
enhanced-select {
--select-border-color: #d1d5db;
--select-focus-border-color: #3b82f6;
--select-background: white;
--select-text-color: #1f2937;
--select-placeholder-color: #9ca3af;
--select-option-hover-background: #f3f4f6;
--select-option-selected-background: #eff6ff;
--select-border-radius: 0.375rem;
--select-padding: 0.5rem 0.75rem;
}Browser Support
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Opera 76+
License
MIT © Navid Rezadoost
