@smilodon/core
v1.5.4
Published
High-performance native select component with extreme-scale virtualization - React, Vue, Svelte, Vanilla JS
Maintainers
Readme
@smilodon/core
📖 Documentation
For comprehensive documentation covering all features, styling options, and advanced patterns:
Additional references:
- Performance Runbook:
docs/PERFORMANCE-RUNBOOK.md - Performance Guide:
docs/PERFORMANCE.md - Known Limitations:
docs/KNOWN-LIMITATIONS.md - Build Tool Integration:
docs/BUILD-TOOL-INTEGRATION.md
The complete guide includes:
- ✅ Complete styling token coverage for colors, layout, chips, motion, and accessibility
- ✅ Vanilla JavaScript patterns (DOM manipulation, event listeners)
- ✅ Complete API reference with all properties and methods
- ✅ CDN usage and module bundler integration
- ✅ Custom renderers with HTML templates
- ✅ Theme examples and dynamic styling
- ✅ Advanced patterns (async loading, local storage, dependent selects)
- ✅ Troubleshooting and accessibility information
WebKit e2e note (Linux/Arch): WebKit Playwright binaries depend on older system libraries; on Arch-based distros we recommend running WebKit tests via the Playwright Docker image.
Why Smilodon?
Smilodon is a Web Component that renders 1,000,000+ items at 60 FPS with constant DOM size, sub-millisecond search, and zero framework lock-in. Built for extreme-scale data applications where legacy libraries crash or lag.
Performance Comparison
| Library | 10K Items | 100K Items | 1M Items | Memory | FPS | |---------|-----------|------------|----------|---------|-----| | Smilodon | 38ms | 81ms | 162ms | 18 MB | 60 | | React Select | 1200ms | ❌ Crash | ❌ Crash | 200+ MB | 10-25 | | Vue Select | 890ms | ❌ Crash | ❌ Crash | 180+ MB | 15-30 | | ng-select | 1100ms | ❌ Crash | ❌ Crash | 220+ MB | 12-28 |
See full benchmarks for methodology and reproducibility.
Installation
npm install @smilodon/coreQuick Start
Vanilla JavaScript / Web Components
<!DOCTYPE html>
<html>
<head>
<script type="module">
import '@smilodon/core';
</script>
</head>
<body>
<smilodon-select id="my-select"></smilodon-select>
<script type="module">
const select = document.getElementById('my-select');
// Generate 1 million items
const items = Array.from({ length: 1_000_000 }, (_, i) => ({
label: `Item ${i + 1}`,
value: i + 1
}));
select.items = items;
select.addEventListener('change', (e) => {
console.log('Selected:', e.detail);
});
</script>
</body>
</html>Framework Usage
@smilodon/core works directly in all frameworks as a Web Component - no adapters needed!
Simply import and use <smilodon-select> in React, Vue, Svelte, or any framework:
// React, Vue, Svelte - all work the same way
import '@smilodon/core';
<smilodon-select ref={selectRef} />Optional Framework Adapters (for enhanced developer experience):
- React:
npm install @smilodon/react- React hooks and components - Vue:
npm install @smilodon/vue- Vue composables and components - Svelte:
npm install @smilodon/svelte- Svelte stores and components - Vanilla:
npm install @smilodon/vanilla- Vanilla JS helpers
These adapters provide framework-native APIs (hooks, composables) for enhanced developer experience, but are not required - the core package works everywhere!
Note: Angular support has been discontinued as of December 2025.
See the main documentation for framework-specific examples.
Key Features
🚀 Extreme Performance
- Constant DOM: Only 10-15 elements rendered regardless of dataset size
- Work Stealing: Background search workers with automatic cancellation
- Sub-millisecond Search: Fenwick tree indexing for O(log n) queries
- 60 FPS Scrolling: Hardware-accelerated virtualization
🎯 Production Ready
- TypeScript First: Complete type definitions included
- Zero Dependencies: 6.6 KB gzipped runtime
- Framework Agnostic: Works with React, Vue, Svelte, or vanilla JS
- Accessibility: WCAG 2.2 AA compliant with ARIA 1.2
- Full keyboard navigation: Enter/Space, Arrows, Home/End, PageUp/Down, Escape
- Multi-select modes: wrap, horizontal, vertical, both (with drag-scroll)
🔒 Enterprise Grade
- SOC2 Compliant: Audit-ready security controls
- CSP Compatible: No eval(), no inline scripts
- SBOM Included: Full dependency transparency
- 99.8% Test Coverage: Unit, integration, and E2E tests
API Reference
Clear Control (Selection/Search Reset)
const select = document.querySelector('enhanced-select');
select.updateConfig({
clearControl: {
enabled: true,
clearSelection: true,
clearSearch: true,
hideWhenEmpty: true,
ariaLabel: 'Clear selected and searched values',
icon: '✕',
}
});
select.addEventListener('clear', (e) => {
console.log(e.detail); // { clearedSelection: boolean, clearedSearch: boolean }
});Style hooks:
- Parts:
::part(clear-button),::part(clear-icon) - Tokens:
--select-clear-button-*,--select-clear-icon-*,--select-input-padding-with-clear
Properties
interface SmilodonSelectElement extends HTMLElement {
items: SelectItem[] | string[] | number[]; // Dataset (can be millions of items)
value: any; // Current selected value
placeholder?: string; // Placeholder text
searchable?: boolean; // Enable search (default: true)
disabled?: boolean; // Disable the select
multiple?: boolean; // Multiple selection mode
virtualization?: boolean; // Enable virtualization (default: true)
maxHeight?: number; // Max dropdown height in pixels
}
interface SelectItem {
label: string; // Display text
value: any; // Value (can be any type)
disabled?: boolean; // Disable this option
group?: string; // Optgroup name
}Flexible Input Formats:
- Object arrays:
[{ value: '1', label: 'Option 1' }, ...]- String arrays:
['Apple', 'Banana', 'Cherry']- automatically converted toSelectItemformat- Number arrays:
[1, 2, 3, 5, 8]- automatically converted toSelectItemformat
Examples with Different Input Types
Object Array (Traditional)
select.items = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'cherry', label: 'Cherry' }
];String Array (Auto-converted)
select.items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];
// Automatically becomes:
// [
// { value: 'Apple', label: 'Apple' },
// { value: 'Banana', label: 'Banana' },
// ...
// ]Number Array (Auto-converted)
select.items = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89];
// Automatically becomes:
// [
// { value: 1, label: '1' },
// { value: 2, label: '2' },
// ...
// ]🎯 Two Ways to Specify Options
Smilodon provides two powerful approaches for defining select options, each optimized for different use cases:
Method 1: Data-Driven (Object Arrays) 📊
Use when: You have structured data and want simple, declarative option rendering.
Advantages:
- ✅ Simple and declarative
- ✅ Auto-conversion from strings/numbers
- ✅ Perfect for basic dropdowns
- ✅ Zero boilerplate code
- ✅ Extremely performant (millions of items)
- ✅ Built-in search and filtering
- ✅ TypeScript type safety
Examples:
// Simple object array
const select = document.querySelector('enhanced-select');
select.items = [
{ value: '1', label: 'Apple' },
{ value: '2', label: 'Banana' },
{ value: '3', label: 'Cherry' }
];
// With additional metadata
select.items = [
{ value: 'us', label: 'United States', disabled: false },
{ value: 'ca', label: 'Canada', disabled: false },
{ value: 'mx', label: 'Mexico', disabled: true } // Disabled option
];
// With grouping
select.items = [
{ value: 'apple', label: 'Apple', group: 'Fruits' },
{ value: 'banana', label: 'Banana', group: 'Fruits' },
{ value: 'carrot', label: 'Carrot', group: 'Vegetables' },
{ value: 'broccoli', label: 'Broccoli', group: 'Vegetables' }
];
// Auto-conversion from strings
select.items = ['Red', 'Green', 'Blue', 'Yellow'];
// Auto-conversion from numbers
select.items = [10, 20, 30, 40, 50];
// Large datasets (millions of items)
select.items = Array.from({ length: 1_000_000 }, (_, i) => ({
value: i,
label: `Item ${i + 1}`
}));Method 2: Component-Driven (Custom Renderers) 🎨
Use when: You need rich, interactive option content with custom HTML/styling.
Advantages:
- ✅ Full control over option rendering
- ✅ Rich content (images, icons, badges, multi-line text)
- ✅ Custom HTML and styling
- ✅ Interactive elements within options
- ✅ Conditional rendering based on item data
- ✅ Perfect for complex UIs (user cards, product listings, etc.)
How it works: Provide an optionTemplate function that returns HTML string for each option.
Examples:
const select = document.querySelector('enhanced-select');
// Example 1: Simple custom template with icons
const items = [
{ value: 'js', label: 'JavaScript', icon: '🟨', description: 'Dynamic scripting language' },
{ value: 'py', label: 'Python', icon: '🐍', description: 'General-purpose programming' },
{ value: 'rs', label: 'Rust', icon: '🦀', description: 'Systems programming language' }
];
select.items = items;
select.optionTemplate = (item, index) => `
<div style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 24px;">${item.icon}</span>
<div>
<div style="font-weight: 600;">${item.label}</div>
<div style="font-size: 12px; color: #6b7280;">${item.description}</div>
</div>
</div>
`;
// Example 2: User selection with avatars
const users = [
{
value: '1',
label: 'John Doe',
email: '[email protected]',
avatar: 'https://i.pravatar.cc/150?img=1',
role: 'Admin'
},
{
value: '2',
label: 'Jane Smith',
email: '[email protected]',
avatar: 'https://i.pravatar.cc/150?img=2',
role: 'User'
},
{
value: '3',
label: 'Bob Johnson',
email: '[email protected]',
avatar: 'https://i.pravatar.cc/150?img=3',
role: 'Moderator'
}
];
select.items = users;
select.optionTemplate = (item, index) => `
<div style="display: flex; align-items: center; gap: 12px; padding: 4px 0;">
<img
src="${item.avatar}"
alt="${item.label}"
style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover;"
/>
<div style="flex: 1;">
<div style="font-weight: 600; color: #1f2937;">${item.label}</div>
<div style="font-size: 13px; color: #6b7280;">${item.email}</div>
</div>
<span style="
padding: 4px 8px;
background: ${item.role === 'Admin' ? '#dbeafe' : '#f3f4f6'};
color: ${item.role === 'Admin' ? '#1e40af' : '#374151'};
border-radius: 12px;
font-size: 11px;
font-weight: 600;
">${item.role}</span>
</div>
`;
// Example 3: Product selection with images and pricing
const products = [
{
value: 'p1',
label: 'Premium Laptop',
price: 1299.99,
stock: 15,
image: 'https://via.placeholder.com/60',
badge: 'Best Seller'
},
{
value: 'p2',
label: 'Wireless Mouse',
price: 29.99,
stock: 150,
image: 'https://via.placeholder.com/60',
badge: null
},
{
value: 'p3',
label: 'Mechanical Keyboard',
price: 89.99,
stock: 0,
image: 'https://via.placeholder.com/60',
badge: 'Out of Stock'
}
];
select.items = products;
select.optionTemplate = (item, index) => `
<div style="display: flex; align-items: center; gap: 12px; opacity: ${item.stock === 0 ? '0.5' : '1'};">
<img
src="${item.image}"
alt="${item.label}"
style="width: 60px; height: 60px; border-radius: 8px; object-fit: cover; border: 1px solid #e5e7eb;"
/>
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-weight: 600; color: #1f2937;">${item.label}</span>
${item.badge ? `
<span style="
padding: 2px 6px;
background: ${item.badge === 'Best Seller' ? '#dcfce7' : '#fee2e2'};
color: ${item.badge === 'Best Seller' ? '#166534' : '#991b1b'};
border-radius: 4px;
font-size: 10px;
font-weight: 600;
">${item.badge}</span>
` : ''}
</div>
<div style="margin-top: 4px; display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 16px; font-weight: 700; color: #059669;">$${item.price.toFixed(2)}</span>
<span style="font-size: 12px; color: #6b7280;">${item.stock > 0 ? `${item.stock} in stock` : 'Out of stock'}</span>
</div>
</div>
</div>
`;
// Example 4: Status indicators with conditional styling
const tasks = [
{ value: 't1', label: 'Design Homepage', status: 'completed', priority: 'high', assignee: 'John' },
{ value: 't2', label: 'API Integration', status: 'in-progress', priority: 'high', assignee: 'Jane' },
{ value: 't3', label: 'Write Documentation', status: 'pending', priority: 'medium', assignee: 'Bob' },
{ value: 't4', label: 'Bug Fixes', status: 'in-progress', priority: 'low', assignee: 'Alice' }
];
const statusColors = {
'completed': { bg: '#dcfce7', color: '#166534', icon: '✓' },
'in-progress': { bg: '#dbeafe', color: '#1e40af', icon: '⟳' },
'pending': { bg: '#fef3c7', color: '#92400e', icon: '○' }
};
const priorityColors = {
'high': '#ef4444',
'medium': '#f59e0b',
'low': '#10b981'
};
select.items = tasks;
select.optionTemplate = (item, index) => {
const status = statusColors[item.status];
return `
<div style="display: flex; align-items: center; gap: 10px; padding: 4px 0;">
<div style="
width: 24px;
height: 24px;
border-radius: 50%;
background: ${status.bg};
color: ${status.color};
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
">${status.icon}</div>
<div style="flex: 1;">
<div style="font-weight: 600; color: #1f2937;">${item.label}</div>
<div style="font-size: 12px; color: #6b7280; margin-top: 2px;">
Assigned to ${item.assignee}
</div>
</div>
<div style="
width: 8px;
height: 8px;
border-radius: 50%;
background: ${priorityColors[item.priority]};
" title="${item.priority} priority"></div>
</div>
`;
};Comparison: When to Use Each Method
| Feature | Method 1: Object Arrays | Method 2: Custom Renderers | |---------|------------------------|---------------------------| | Setup Complexity | ⭐ Simple | ⭐⭐ Moderate | | Rendering Speed | ⭐⭐⭐ Fastest | ⭐⭐ Fast | | Visual Customization | ⭐⭐ Limited | ⭐⭐⭐ Unlimited | | Use Case | Standard dropdowns | Rich, complex UIs | | Code Amount | Minimal | More code | | TypeScript Support | ⭐⭐⭐ Full | ⭐⭐⭐ Full | | Performance (1M items) | ⭐⭐⭐ Excellent | ⭐⭐ Good | | Learning Curve | ⭐ Easy | ⭐⭐ Medium |
Best Practices:
✅ Use Method 1 (Object Arrays) when:
- You need simple text-based options
- Performance is critical (millions of items)
- You want minimal code
- Built-in search/filter is sufficient
✅ Use Method 2 (Custom Renderers) when:
- You need images, icons, or badges
- Options require multiple lines of text
- Custom styling/layout is important
- Conditional rendering based on data
- Rich user experience is priority
Combining Both Methods
You can start with Method 1 and add Method 2 later as your UI evolves:
// Start simple
select.items = ['Option 1', 'Option 2', 'Option 3'];
// Later, add custom rendering without changing items
select.optionTemplate = (item, index) => `
<div style="padding: 8px; background: ${index % 2 ? '#f9fafb' : 'white'};">
<strong>${item.label || item}</strong>
</div>
`;Events
// Selection changed
select.addEventListener('change', (event: CustomEvent) => {
console.log(event.detail); // { value, label }
});
// Dropdown opened
select.addEventListener('open', () => {
console.log('Dropdown opened');
});
// Dropdown closed
select.addEventListener('close', () => {
console.log('Dropdown closed');
});
// Search query changed
select.addEventListener('search', (event: CustomEvent) => {
console.log(event.detail.query);
});Methods
// Programmatically open/close
select.open();
select.close();
// Clear selection
select.clear();
// Focus the select
select.focus();
// Get filtered items (useful for debugging)
const filtered = select.getFilteredItems();Advanced Usage
Custom Styling
Smilodon uses CSS custom properties (CSS variables) for easy theming and customization. The default theme is light mode with a clean white background.
Basic Customization
enhanced-select {
/* Options styling */
--select-option-bg: #ffffff;
--select-option-color: #1f2937;
--select-option-padding: 8px 12px;
/* Hover state */
--select-option-hover-bg: #f3f4f6;
--select-option-hover-color: #1f2937;
/* Selected state */
--select-option-selected-bg: #e0e7ff;
--select-option-selected-color: #4338ca;
--select-option-selected-border: 1px solid #4338ca;
--select-option-selected-hover-bg: #c7d2fe;
--select-option-selected-hover-border: 1px solid #3730a3;
/* Active/focused state */
--select-option-active-bg: #f3f4f6;
--select-option-active-color: #1f2937;
/* Dropdown */
--select-dropdown-bg: white;
--select-dropdown-border: #ccc;
--select-dropdown-shadow: 0 4px 6px rgba(0,0,0,0.1);
}Dark Mode (Opt-in)
Dark mode is opt-in only and can be enabled by adding a class or data attribute:
<!-- Using class -->
<enhanced-select class="dark-mode"></enhanced-select>
<!-- Using data attribute -->
<enhanced-select data-theme="dark"></enhanced-select>
<!-- Also supported (attribute aliases) -->
<enhanced-select dark-mode></enhanced-select>
<enhanced-select darkmode></enhanced-select>
<enhanced-select theme="dark"></enhanced-select>/* Custom dark mode colors */
enhanced-select.dark-mode {
--select-dark-bg: #1f2937;
--select-dark-text: #f9fafb;
--select-dark-border: #4b5563;
--select-dark-dropdown-bg: #1f2937;
--select-dark-option-color: #f9fafb;
--select-dark-option-bg: #1f2937;
--select-dark-option-hover-bg: #374151;
--select-dark-option-selected-bg: #3730a3;
}Available CSS Variables
Light Mode (Default)
--select-options-bg /* Options container background (white) */
--select-width /* Host width (100% default) */
--select-height /* Host height (auto default) */
--select-option-color /* Option text color (#1f2937) */
--select-option-bg /* Option background (white) */
--select-option-padding /* Option padding (8px 12px) */
--select-option-hover-bg /* Hover background (#f3f4f6) */
--select-option-hover-color /* Hover text color (#1f2937) */
--select-option-selected-bg /* Selected background (#e0e7ff) */
--select-option-selected-color /* Selected text color (#4338ca) */
--select-option-selected-border /* Selected border (inherits option border by default) */
--select-option-selected-hover-bg /* Selected+hover background (inherits selected bg by default) */
--select-option-selected-hover-border /* Selected+hover border (inherits selected border by default) */
--select-option-active-bg /* Active background (#f3f4f6) */
--select-option-active-color /* Active text color (#1f2937) */
--select-input-width /* Input field width */
--select-input-height /* Input container height */
--select-dropdown-bg /* Dropdown background (white) */
--select-dropdown-border /* Dropdown border color (#ccc) */
--select-dropdown-shadow /* Dropdown shadow */
--select-empty-padding /* Empty/no-results container padding */
--select-empty-color /* Text color for empty/no-results models */
--select-empty-font-size /* Font size */
--select-empty-bg /* Background for empty/no-results state */
--select-empty-min-height /* Minimum height of empty state box */
/* Arrow/button */
--select-arrow-size /* Width & height of SVG icon (16px default) */
--select-arrow-color /* Icon color (#667eea) */
--select-arrow-hover-color /* Icon color when hovered (#667eea) */
--select-arrow-hover-bg /* Background when hover (rgba(102,126,234,0.08)) */
--select-arrow-width /* Container width (40px) */
--select-arrow-border-radius /* Container border radius */
/* Group headers (when using groupedItems or flat items with `group` property) */
--select-group-header-padding /* Padding inside header (8px 12px) */
--select-group-header-separator-margin-top /* Extra gap before later groups */
--select-group-header-separator-padding-top /* Adds separator spacing without changing header title padding */
--select-group-header-separator-border-top /* Divider above later groups */
--select-group-header-color /* Text color (#6b7280) */
--select-group-header-bg /* Background (#f3f4f6) */
--select-group-header-font-size
--select-group-header-text-align /* Header text alignment (left default) */
--select-group-header-text-transform
--select-group-header-letter-spacing
--select-group-header-border-bottom
/* Option content and checkmark hooks */
--select-option-content-overflow
--select-option-content-text-overflow
--select-option-content-white-space
--select-checkmark-margin-left
--select-checkmark-colorDark Mode (Opt-in)
--select-dark-bg /* Dark input background (#1f2937) */
--select-dark-text /* Dark text color (#f9fafb) */
--select-dark-border /* Dark border color (#4b5563) */
--select-dark-dropdown-bg /* Dark dropdown background (#1f2937) */
--select-dark-options-bg /* Dark options container bg (#1f2937) */
--select-dark-option-color /* Dark option text (#f9fafb) */
--select-dark-option-bg /* Dark option background (#1f2937) */
--select-dark-option-hover-bg /* Dark hover background (#374151) */
--select-dark-option-selected-bg /* Dark selected bg (#3730a3) */
--select-dark-group-header-color /* Dark header text */
--select-dark-group-header-bg /* Dark header background */
--select-dark-badge-bg /* Dark badge background */
--select-dark-badge-color /* Dark badge text */
--select-dark-arrow-color /* Dark arrow icon color */
--select-dark-arrow-bg /* Dark arrow background */
--select-dark-arrow-hover-bg /* Dark arrow hover background */
--select-dark-separator-bg /* Dark separator color */
--select-dark-separator-width /* Dark separator width */
--select-dark-empty-color /* Dark empty state text */Keyboard Navigation & Accessibility
Smilodon provides full keyboard accessibility (WCAG 2.2 AA compliant):
| Key | Action |
|-----|--------|
| Enter / Space | Open dropdown or select focused option |
| ↑ / ↓ | Navigate through options |
| Home / End | Jump to first/last option |
| PageUp / PageDown | Jump 10 options up/down |
| Escape | Close dropdown |
| Tab | Close dropdown and move focus |
| Ctrl+A (multi-select) | Select all options |
| Type characters | Type-ahead search |
Multi-Select Range Selection:
Shift + ↑/↓- Range selectionCtrl/Cmd + Click- Toggle individual itemsShift + Home/End- Select from current to first/last
Multi-Select Display Modes
Control badge scrolling behavior in multi-select mode with multiSelectDisplay config:
{
multiSelectDisplay: {
mode: 'wrap', // 'wrap' | 'horizontal' | 'vertical' | 'both'
maxHeight: '160px',
overflowX: 'auto',
overflowY: 'auto',
dragScroll: true // Enable drag-to-scroll (horizontal mode)
}
}Display Modes:
wrap(default) - Badges wrap to new lines, vertical scroll when neededhorizontal- Single row, horizontal scroll, drag-to-scroll enabledvertical- Single column, vertical scroll onlyboth- Both horizontal and vertical scroll, dense packing
Advanced Styling Controls
Disabling Hover Animation
To remove hover effects on options:
enhanced-select {
--select-option-hover-bg: transparent;
--select-option-hover-color: inherit;
--select-option-hover-transform: none;
}Multi-Select Container & Badge Padding
Control container padding separately from badge padding:
enhanced-select {
--select-multi-container-padding: 8px; /* Container internal padding */
--select-badge-padding: 4px 8px; /* Individual badge padding */
--select-input-gap: 6px; /* Gap between badges */
}Separator Dimension Controls
Full control over the vertical separator line (works in all modes):
enhanced-select {
--select-separator-width: 2px; /* Line thickness */
--select-separator-height: 60%; /* Line height for single-select (% of container) */
--select-multi-separator-height: 40px; /* Fixed height for multi-select (default: auto) */
--select-separator-display: none; /* Hide separator completely */
--select-separator-bg: #e5e7eb; /* Custom color/gradient */
--select-separator-opacity: 0.8; /* Line opacity */
}Note: Multi-select uses --select-multi-separator-height (default: auto to stretch with container). Set a fixed pixel value like 40px to maintain consistent separator height regardless of how many items are selected.
Input & Badge Typography
enhanced-select {
/* Input field text */
--select-input-font-size: 14px;
--select-input-font-weight: 400;
/* Badge text */
--select-badge-font-size: 13px;
--select-badge-font-weight: 500;
--select-badge-line-height: 1.2;
}Complete CSS Variables Reference
The canonical styling inventory now lives in:
- docs/STYLING.md for strategy and examples
- docs/STYLING-TOKENS.md for the exhaustive token table
That reference covers:
- Theme foundation tokens (palette, shadows, radii, timing)
- Input shell, separator, arrow, and clear-control hooks
- Multi-select chip structure, remove-button states, and badge motion
- Dropdown, scrollbar, group-header, and option-state hooks
- Empty, loading, searching, error, reduced-motion, and high-contrast hooks
Highlighted Customization Features
Separator Line Between Input and Arrow The vertical separator line that appears between the input area and the dropdown arrow is fully customizable:
enhanced-select {
/* Customize the separator line */
--select-separator-width: 2px; /* Line thickness */
--select-separator-height: 80%; /* Line height */
--select-separator-position: 40px; /* Distance from right edge */
--select-separator-gradient: linear-gradient(
to bottom,
transparent 0%,
#3b82f6 20%,
#3b82f6 80%,
transparent 100%
);
}
/* Typo-compatible aliases are also accepted */
enhanced-select {
--select-seperator-width: 2px;
--select-seperator-height: 80%;
--select-seperator-position: 40px;
--select-seperator-gradient: linear-gradient(to bottom, transparent 0%, #3b82f6 20%, #3b82f6 80%, transparent 100%);
}Gradient Dropdown + Hover/Selected States
If your dropdown uses a gradient background (for example via --select-dropdown-bg), option hover/selected colors still work as expected. The component intentionally uses the background shorthand for hover/selected option states so any inherited background-image layers are cleared correctly.
Closed Input Value + Dropdown Option Alignment You can switch alignment and inspect both the selected value in the closed input and the dropdown options with matching tokens:
enhanced-select.align-center {
--select-input-text-align: center;
--select-option-text-align: center;
}
enhanced-select.align-right {
--select-input-text-align: right;
--select-option-text-align: right;
}Useful related hooks:
--select-input-text-align--select-input-justify-content--select-option-text-align--select-group-header-text-align
This is especially useful when testing centered or right-aligned UI layouts, because you can verify the closed state and open dropdown state together.
Dropdown Placement: Bottom / Top / Automatic The dropdown can be opened in three modes:
bottom— always below the selecttop— always above the selectauto— below if there is enough room below, otherwise above
Per-instance:
select.updateConfig({
dropdownPlacement: {
mode: 'auto',
},
});Global default:
configureSelect({
dropdownPlacement: {
mode: 'top',
},
});Related styling hooks:
--select-dropdown-top--select-dropdown-bottom--select-dropdown-transform-origin--select-dropdown-top-transform-origin
Direction: LTR / RTL
The select supports both ltr and rtl directions. The default is ltr.
Global default:
configureSelect({
direction: 'rtl',
});Per-instance:
select.updateConfig({
direction: 'rtl',
});RTL support mirrors the shell and dropdown layout, including arrow placement, clear control placement, separator side, selected indicator side, and chip remove spacing. The host dir attribute is kept in sync automatically.
Badge Remove/Delete Button (Multi-Select) The × button that removes selected items in multi-select mode is fully customizable:
Group Header & No‑Results Parts
Both the group header and the no-results message are exposed as shadow parts (group-header and no-results) so you can target them with ::part() selectors or CSS variables. This makes it straightforward to match the look of your host framework or UI kit.
enhanced-select {
/* Customize badge appearance */
--select-badge-bg: #10b981; /* Badge background */
--select-badge-color: white; /* Badge text color */
--select-badge-width: auto; /* Explicit badge width */
--select-badge-height: 32px; /* Explicit badge height */
--select-badge-padding: 6px 10px; /* Badge spacing */
--select-badge-margin: 4px; /* Badge outer spacing */
--select-badge-border-radius: 10px; /* Badge radius */
/* Customize the × (remove/delete) button */
--select-badge-remove-size: 18px; /* Button size */
--select-badge-remove-icon-size: 12px; /* Icon glyph / SVG size */
--select-badge-remove-bg: rgba(255, 255, 255, 0.3); /* Button background */
--select-badge-remove-color: white; /* × symbol color */
--select-badge-remove-font-size: 18px; /* × symbol size */
--select-badge-remove-hover-bg: rgba(255, 255, 255, 0.6); /* Hover state */
}You can also replace the default × with custom SVG or text:
select.updateConfig({
selection: {
removeButtonIcon: '<svg viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/></svg>'
}
});Runtime style config now also supports higher-level sections for:
badgebadgeHoverbadgeActivebadgeLabelbadgeRemovebadgeRemoveHoverbadgeRemoveActivegroupHeaderactiveOption
This means badge size, badge radius, remove-button size, group-header layout, and active-option border/radius can be controlled either with CSS tokens or with updateConfig({ styles: ... }).
Real-World Customization Examples
Example 1: Bootstrap-style Select
enhanced-select {
--select-input-border: 1px solid #ced4da;
--select-input-border-radius: 0.375rem;
--select-input-focus-border: #86b7fe;
--select-shadow-focus: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
--select-option-hover-bg: #e9ecef;
--select-option-selected-bg: #0d6efd;
--select-option-selected-color: white;
--select-badge-bg: #0d6efd;
}Example 2: Material Design
enhanced-select {
--select-input-border-radius: 4px;
--select-input-focus-border: #1976d2;
--select-shadow-focus: none;
--select-option-padding: 16px;
--select-option-hover-bg: rgba(0, 0, 0, 0.04);
--select-option-selected-bg: #e3f2fd;
--select-option-selected-color: #1976d2;
--select-badge-bg: #1976d2;
--select-badge-border-radius: 16px;
}Example 3: Tailwind-style
enhanced-select {
--select-input-border: 1px solid #e5e7eb;
--select-input-border-radius: 0.5rem;
--select-input-focus-border: #3b82f6;
--select-shadow-focus: 0 0 0 3px rgba(59, 130, 246, 0.1);
--select-option-padding: 0.5rem 0.75rem;
--select-option-hover-bg: #f3f4f6;
--select-option-selected-bg: #dbeafe;
--select-option-selected-color: #1e40af;
}Framework compatibility guidance also lives in ../../docs/CSS-FRAMEWORK-COMPATIBILITY.md.
Example 4: Custom Brand Colors
enhanced-select {
/* Your brand colors */
--select-input-focus-border: #ff6b6b;
--select-arrow-color: #ff6b6b;
--select-badge-bg: #ff6b6b;
--select-option-selected-bg: #ffe0e0;
--select-option-selected-color: #c92a2a;
--select-button-border: 1px solid #ff6b6b;
--select-button-color: #ff6b6b;
--select-button-hover-bg: #ff6b6b;
}Framework-Specific Examples
React - Customizing Separator & Badge Remove Button
import { Select } from '@smilodon/react';
function App() {
return (
<Select
items={items}
multiple
style={{
'--select-option-hover-bg': '#2563eb',
'--select-option-padding': '12px 16px',
'--select-badge-bg': '#3b82f6',
'--select-badge-remove-bg': 'rgba(255, 255, 255, 0.4)',
'--select-badge-remove-hover-bg': 'rgba(255, 255, 255, 0.7)',
'--select-separator-gradient': 'linear-gradient(to bottom, transparent 0%, #3b82f6 20%, #3b82f6 80%, transparent 100%)'
}}
/>
);
}Vue - Complete Customization
<template>
<Select
:items="items"
multiple
:style="{
'--select-option-hover-bg': '#2563eb',
'--select-option-padding': '12px 16px',
'--select-badge-bg': '#3b82f6',
'--select-badge-remove-size': '20px',
'--select-badge-remove-bg': 'rgba(255, 255, 255, 0.4)',
'--select-separator-width': '2px',
'--select-separator-height': '70%'
}"
/>
</template>Svelte - Themed Components
<script>
import { Select } from '@smilodon/svelte';
</script>
<Select
items={items}
multiple
style="
--select-badge-bg: #10b981;
--select-badge-remove-bg: rgba(255, 255, 255, 0.3);
--select-badge-remove-hover-bg: rgba(255, 255, 255, 0.6);
--select-separator-gradient: linear-gradient(to bottom, transparent, #10b981, transparent);
"
/>Vanilla JS - Dynamic Styling
const select = document.querySelector('enhanced-select');
// Apply custom separator and badge styles
select.style.setProperty('--select-separator-width', '2px');
select.style.setProperty('--select-separator-gradient', 'linear-gradient(to bottom, transparent, #ff6b6b, transparent)');
select.style.setProperty('--select-badge-bg', '#ff6b6b');
select.style.setProperty('--select-badge-remove-size', '18px');
select.style.setProperty('--select-badge-remove-bg', 'rgba(255, 255, 255, 0.3)');Server-Side Rendering (SSR)
Smilodon gracefully handles SSR environments:
// Check if running in browser
if (typeof window !== 'undefined') {
import('@smilodon/core').then(() => {
// Initialize after hydration
});
}Performance Monitoring
Enable telemetry to monitor performance:
select.enableTelemetry = true;
select.addEventListener('telemetry', (event) => {
console.log('Performance metrics:', event.detail);
// { renderTime, searchTime, scrollFPS, memoryUsage }
});Architecture
Smilodon's performance comes from three core optimizations:
- Virtual Scrolling: Only renders visible items (10-15 DOM nodes for any dataset size)
- Work Stealing Scheduler: Search operations run in background workers with automatic cancellation
- Fenwick Tree Indexing: O(log n) search queries instead of O(n) array scans
Read the full architecture guide for implementation details.
Browser Support
| Browser | Version | |---------|---------| | Chrome | 90+ | | Firefox | 88+ | | Safari | 14.1+ | | Edge | 90+ |
Requires browsers with native Web Components support (custom elements v1).
Bundle Size
- Runtime: 6.6 KB gzipped (ESM)
- Full Package: 365 KB (includes 5 formats + source maps + types)
- Tree-shakeable: Import only what you need
TypeScript
Full TypeScript definitions included. No need for @types/* packages.
import type { SmilodonSelectElement, SelectItem } from '@smilodon/core';
const items: SelectItem[] = [
{ label: 'Option 1', value: 1 },
{ label: 'Option 2', value: 2 }
];
const select = document.querySelector('smilodon-select') as SmilodonSelectElement;
select.items = items;Documentation
- Main README: Complete guide with framework examples
- Benchmarks: Transparent performance comparisons
- API Reference: Full API documentation
- Architecture: Technical deep-dive
- Migration Guide: Migrate from React Select, Vue Select, etc.
Examples
Visit the interactive playground to see Smilodon in action with:
- 1M item datasets
- Real-time search
- Framework integrations
- Custom styling
Contributing
Contributions are welcome! Please read our Contributing Guide for details.
License
MIT © Navid Rezadoost
Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Security: Security Policy
