@smilodon/svelte
v1.9.2
Published
Production-ready, accessible select component for Svelte - part of the Smilodon UI toolkit
Downloads
265
Maintainers
Readme
@smilodon/svelte
Production-ready, accessible select component for Svelte applications. Part of the Smilodon UI toolkit.
📖 Documentation
Live documentation & interactive examples: navidrezadoost.github.io/Smilodon
Framework integration (Vue 3, Nuxt 4, React, SSR, Vite): docs/FRAMEWORK-INTEGRATION.md
For comprehensive documentation covering all features, styling options, and advanced patterns:
The complete guide includes:
- ✅ Complete styling token coverage for colors, layout, chips, motion, and accessibility
- ✅ Svelte-specific patterns (reactive statements, stores, bind:value)
- ✅ Complete API reference with TypeScript types
- ✅ Svelte stores integration (writable, derived)
- ✅ Custom renderers returning HTML strings
- ✅ Theme examples with :global() styling
- ✅ Advanced patterns (Context API, reactive dependencies)
- ✅ Troubleshooting and accessibility information
Features
- New:
optionRendererprop (Option B) lets you return anHTMLElementfor custom DOM-driven option layouts.
Advanced scenarios (Svelte)
A11y-first:
<label id="city-label" for="city-picker">City</label> <Select id="city-picker" aria-labelledby="city-label" />Server-side lookup:
<Select bind:this={picker} searchable on:search={(e) => load(e.detail.query)} /> async function load(q) { const res = await fetch(`/api/cities?q=${encodeURIComponent(q)}`); const items = await res.json(); picker?.setItems(items); }Heavy lists (100k+):
<Select {items} virtualized estimatedItemHeight={44} /> <script> export let items = Array.from({ length: 100_000 }, (_, i) => ({ value: i, label: `Item ${i}` })); </script>✨ Single & Multi-Select - Choose one or multiple options
🔍 Searchable - Filter options with built-in or custom search
♿ Fully Accessible - WCAG 2.1 AAA compliant
⚡ Virtual Scrolling - Handle 100k+ options smoothly
📜 Infinite Scroll - Load data progressively
👥 Grouped Options - Organize options into categories
🎨 Customizable - Style with CSS variables or custom themes
📦 Tiny Bundle - ~2KB gzipped
🔧 TypeScript - Full type safety included
Installation
npm install @smilodon/svelte @smilodon/coreSvelteKit / SSR Notes
The Svelte adapter waits for enhanced-select to be defined during onMount(), so it is safe to use in SvelteKit pages as long as imperative browser-only code stays inside lifecycle hooks.
Recommended pattern
<script lang="ts">
import { onMount } from 'svelte';
import { Select } from '@smilodon/svelte';
let items = [];
let value = [];
onMount(async () => {
items = await fetch('/api/frameworks').then((response) => response.json());
});
</script>
<Select {items} bind:value searchable multiple clearable />SSR guidance
- Avoid direct
window,document, or custom-element access outsideonMount(). - Use
bind:valuefor regular selection flow and exported instance methods for imperative control. - Prefer
optionRendererwhen you need DOM-driven custom options and plain markup when you do not.
Quick Start
<script lang="ts">
import { Select } from '@smilodon/svelte';
const items = [
{ value: '1', label: 'Apple' },
{ value: '2', label: 'Banana' },
{ value: '3', label: 'Cherry' },
];
let selectedValue;
</script>
<Select {items} bind:value={selectedValue} placeholder="Select a fruit..." />Clear Control (Reset Selected/Search Value)
<script lang="ts">
import { Select } from '@smilodon/svelte';
let value = '';
const items = [
{ value: 'js', label: 'JavaScript' },
{ value: 'py', label: 'Python' },
{ value: 'rs', label: 'Rust' },
];
</script>
<Select
{items}
bind:value
searchable
clearable
clearSelectionOnClear
clearSearchOnClear
clearAriaLabel="Clear selected and searched values"
clearIcon="✕"
on:clear={(e) => console.log('cleared', e.detail)}
/>Styling hooks: ::part(clear-button) and ::part(clear-icon) plus --select-clear-* CSS tokens.
An empty clearable field reserves no clear-button space by default. The control and its coordinated input/arrow spacing appear when a selected value or enabled search query can be cleared, then disappear once both targets are empty. The same rule applies to single, multi-select, LTR, and RTL layouts.
Use full config passthrough when a permanently visible disabled-when-empty action is required:
<Select
{items}
bind:value
clearable
config={{ clearControl: { hideWhenEmpty: false } }}
/>The clear event reports clearedSelection and clearedSearch. Style the control through its parts and --select-clear-* tokens rather than permanent input padding.
Direction and option-state controls
The Svelte adapter exposes the same runtime controls as the shared core:
<Select
{items}
bind:value
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 internal selector overrides
Core config parity and global defaults
The Svelte adapter now accepts the same advanced runtime configuration model as the shared core.
Convenience props
Use the Svelte props below for the most common advanced core features:
selectionConfigmultiSelectDisplayscrollToSelectedstylesconfig
<script lang="ts">
import { Select } from '@smilodon/svelte';
let value: Array<string | number> = ['react', 'solid'];
const items = [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
{ value: 'svelte', label: 'Svelte' },
{ value: 'solid', label: 'Solid' },
];
</script>
<Select
{items}
bind:value
multiple
searchable
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="−"
placeholder="Select frameworks"
/>Full config passthrough
<Select
{items}
bind:value
multiple
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/svelte
import { configureSelect } from '@smilodon/svelte';
configureSelect({
searchable: true,
clearControl: {
enabled: true,
clearSelection: true,
clearSearch: true,
},
selection: {
showSelectedIndicator: false,
removeButtonIcon: '×',
},
});bind:this runtime methods
The Svelte component exports additional runtime helpers beyond open() and clear().
<script lang="ts">
import { Select } from '@smilodon/svelte';
let picker: Select;
function runRuntimeActions() {
picker?.clearSearch();
picker?.updateCoreConfig({ selection: { showSelectedIndicator: false } });
picker?.setError('Please review this field');
console.log(picker?.validate());
}
</script>
<Select bind:this={picker} {items} required searchable />
<button on:click={runRuntimeActions}>Run runtime actions</button>Examples
Searchable Select
<script lang="ts">
import { Select } from '@smilodon/svelte';
const items = [
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'mx', label: 'Mexico' },
];
let country;
</script>
<Select
{items}
bind:value={country}
searchable
placeholder="Search countries..."
/>Multi-Select
<script lang="ts">
import { Select } from '@smilodon/svelte';
const items = [
{ value: 'js', label: 'JavaScript' },
{ value: 'ts', label: 'TypeScript' },
{ value: 'py', label: 'Python' },
];
let languages = [];
</script>
<Select
{items}
bind:value={languages}
multiple
placeholder="Select languages..."
/>Grouped Options
<script lang="ts">
import { Select } from '@smilodon/svelte';
const groupedItems = [
{
group: 'Fruits',
items: [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
],
},
{
group: 'Vegetables',
items: [
{ value: 'carrot', label: 'Carrot' },
{ value: 'potato', label: 'Potato' },
],
},
];
let food;
</script>
<Select {groupedItems} bind:value={food} />Infinite Scroll
<script lang="ts">
import { Select } from '@smilodon/svelte';
let items = generateItems(1, 50);
let page = 1;
function handleLoadMore() {
page++;
const newItems = generateItems(page * 50 + 1, (page + 1) * 50);
items = [...items, ...newItems];
}
function generateItems(start, end) {
return Array.from({ length: end - start + 1 }, (_, i) => ({
value: start + i,
label: `Item ${start + i}`,
}));
}
</script>
<Select
{items}
infiniteScroll
pageSize={50}
on:loadMore={handleLoadMore}
placeholder="Scroll to load more..."
/>Virtual Scrolling (Large Datasets)
<script lang="ts">
import { Select } from '@smilodon/svelte';
// Generate 100,000 items
const items = Array.from({ length: 100000 }, (_, i) => ({
value: i + 1,
label: `Item ${i + 1}`,
}));
</script>
<Select
{items}
virtualized
searchable
placeholder="100k items with virtual scrolling..."
/>Server-Side Search
<script lang="ts">
import { Select } from '@smilodon/svelte';
let items = [];
let isLoading = false;
async function handleSearch(event) {
const query = event.detail.query;
if (!query) {
items = [];
return;
}
isLoading = true;
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
items = data.results;
isLoading = false;
}
</script>
<Select
{items}
searchable
on:search={handleSearch}
placeholder="Type to search..."
/>Controlled Component
<script lang="ts">
import { Select } from '@smilodon/svelte';
const items = [
{ value: 'draft', label: 'Draft' },
{ value: 'published', label: 'Published' },
{ value: 'archived', label: 'Archived' },
];
let status = 'draft';
function handleChange(event) {
console.log('Status changed:', event.detail.value);
status = event.detail.value;
}
</script>
<Select
{items}
value={status}
on:change={handleChange}
/>
<button on:click={() => status = 'published'}>
Publish
</button>Imperative API
<script lang="ts">
import { Select } from '@smilodon/svelte';
let selectRef;
const items = [...];
function programmaticOpen() {
selectRef.open();
}
function programmaticClear() {
selectRef.clear();
}
</script>
<Select bind:this={selectRef} {items} />
<button on:click={programmaticOpen}>Open Select</button>
<button on:click={programmaticClear}>Clear Selection</button>Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| items | SelectItem[] | [] | Array of selectable items |
| groupedItems | GroupedItem[] | undefined | Grouped items array |
| value | string \| number \| Array | undefined | Controlled value |
| defaultValue | string \| number \| Array | undefined | Initial value (uncontrolled) |
| 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 (infinite scroll) |
| virtualized | boolean | true | Enable virtual scrolling |
| maxSelections | number | undefined | Max selections (multi-select) |
| placement | 'bottom' \| 'top' \| 'auto' | 'auto' | Dropdown placement |
| className | string | '' | Custom CSS class |
| style | string | '' | Custom inline styles |
Events
| Event | Detail | Description |
|-------|--------|-------------|
| change | { value, selectedItems } | Value changed |
| select | { item, index } | Item selected |
| open | void | Dropdown opened |
| close | void | Dropdown closed |
| search | { query } | Search query changed |
| loadMore | { page } | Load more requested |
| create | { value } | New item creation |
Methods
Access these methods using bind:this:
| Method | Signature | Description |
|--------|-----------|-------------|
| open() | () => void | Open the dropdown |
| close() | () => void | Close the dropdown |
| focus() | () => void | Focus the select |
| clear() | () => void | Clear selection |
| setItems() | (items: SelectItem[]) => void | Update items |
| setGroupedItems() | (groups: GroupedItem[]) => void | Update grouped items |
Types
interface SelectItem {
value: string | number;
label: string;
disabled?: boolean;
group?: string;
[key: string]: any;
}
interface GroupedItem {
group: string;
items: SelectItem[];
}🎯 Two Ways to Specify Options
Smilodon Svelte 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 - Svelte-friendly
- ✅ Auto-conversion from strings/numbers
- ✅ Perfect for basic dropdowns
- ✅ Works seamlessly with Svelte stores
- ✅ Extremely performant (millions of items)
- ✅ Built-in search and filtering
- ✅ Full TypeScript type safety
Examples:
<script lang="ts">
import { Select } from '@smilodon/svelte';
// Example 1: Simple object array
let value = '';
const items = [
{ value: '1', label: 'Apple' },
{ value: '2', label: 'Banana' },
{ value: '3', label: 'Cherry' }
];
</script>
<Select
items={items}
bind:value
placeholder="Select a fruit..."
/><script lang="ts">
import { Select } from '@smilodon/svelte';
// Example 2: With metadata and disabled options
let country = '';
const countries = [
{ value: 'us', label: 'United States', disabled: false },
{ value: 'ca', label: 'Canada', disabled: false },
{ value: 'mx', label: 'Mexico', disabled: true }
];
</script>
<Select
items={countries}
bind:value={country}
placeholder="Select a country..."
/><script lang="ts">
import { Select } from '@smilodon/svelte';
// Example 3: With grouping
let food = '';
const foods = [
{ value: 'apple', label: 'Apple', group: 'Fruits' },
{ value: 'banana', label: 'Banana', group: 'Fruits' },
{ value: 'carrot', label: 'Carrot', group: 'Vegetables' },
{ value: 'broccoli', label: 'Broccoli', group: 'Vegetables' }
];
</script>
<Select
items={foods}
bind:value={food}
placeholder="Select food..."
/><script lang="ts">
import { Select } from '@smilodon/svelte';
// Example 4: Auto-conversion from strings
let color = '';
const colors = ['Red', 'Green', 'Blue', 'Yellow'];
</script>
<Select
items={colors}
bind:value={color}
placeholder="Select a color..."
/><script lang="ts">
import { Select } from '@smilodon/svelte';
// Example 5: Auto-conversion from numbers
let size: number | string = '';
const sizes = [10, 20, 30, 40, 50];
</script>
<Select
items={sizes}
bind:value={size}
placeholder="Select size..."
/><script lang="ts">
import { Select } from '@smilodon/svelte';
// Example 6: Large datasets with reactive statement
let id = '';
$: items = Array.from({ length: 100_000 }, (_, i) => ({
value: i.toString(),
label: `Item ${i + 1}`
}));
</script>
<Select
items={items}
bind:value={id}
virtualized
placeholder="Select from 100K items..."
/>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
- ✅ Reactive data binding
- ✅ Conditional rendering based on item data
- ✅ Works with Svelte stores
- ✅ Perfect for complex UIs (user cards, product listings, etc.)
How it works: Provide an optionTemplate function that returns HTML string for each option.
Examples:
<script lang="ts">
import { Select, type SelectItem } from '@smilodon/svelte';
// Example 1: Simple custom template with icons
interface Language extends SelectItem {
icon: string;
description: string;
}
let lang = '';
const languages: Language[] = [
{ 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' }
];
const languageRenderer = (item: Language, index: number) => `
<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>
`;
</script>
<Select
items={languages}
bind:value={lang}
optionTemplate={languageRenderer}
placeholder="Select a language..."
/><script lang="ts">
import { Select, type SelectItem } from '@smilodon/svelte';
// Example 2: User selection with avatars
interface User extends SelectItem {
email: string;
avatar: string;
role: 'Admin' | 'User' | 'Moderator';
}
let userId = '';
const users: User[] = [
{
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'
}
];
const userRenderer = (item: User) => `
<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>
`;
</script>
<Select
items={users}
bind:value={userId}
optionTemplate={userRenderer}
placeholder="Select a user..."
/><script lang="ts">
import { Select, type SelectItem } from '@smilodon/svelte';
// Example 3: Product selection with images and pricing
interface Product extends SelectItem {
price: number;
stock: number;
image: string;
badge?: string;
}
let productId = '';
const products: Product[] = [
{
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'
},
{
value: 'p3',
label: 'Mechanical Keyboard',
price: 89.99,
stock: 0,
image: 'https://via.placeholder.com/60',
badge: 'Out of Stock'
}
];
const productRenderer = (item: Product) => `
<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>
`;
</script>
<Select
items={products}
bind:value={productId}
optionTemplate={productRenderer}
placeholder="Select a product..."
/><script lang="ts">
import { Select, type SelectItem } from '@smilodon/svelte';
// Example 4: Status indicators with conditional styling
interface Task extends SelectItem {
status: 'completed' | 'in-progress' | 'pending';
priority: 'high' | 'medium' | 'low';
assignee: string;
}
let taskId = '';
const tasks: Task[] = [
{ 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 statusConfig = {
'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'
};
const taskRenderer = (item: Task) => {
const status = statusConfig[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>
`;
};
</script>
<Select
items={tasks}
bind:value={taskId}
optionTemplate={taskRenderer}
placeholder="Select a task..."
/><script lang="ts">
import { Select, type SelectItem } from '@smilodon/svelte';
import { writable } from 'svelte/store';
// Example 5: Using Svelte stores
interface Tag extends SelectItem {
color: string;
count: number;
}
const tag = writable('');
const tags: Tag[] = [
{ value: 'react', label: 'React', color: 'blue', count: 1250 },
{ value: 'vue', label: 'Vue', color: 'green', count: 850 },
{ value: 'angular', label: 'Angular', color: 'red', count: 420 }
];
const tagRenderer = (item: Tag) => `
<div style="display: flex; align-items: center; justify-content: space-between; padding: 8px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="width: 12px; height: 12px; border-radius: 50%; background: ${item.color};"></span>
<span style="font-weight: 600; color: #1f2937;">${item.label}</span>
</div>
<span style="font-size: 14px; color: #6b7280;">${item.count} posts</span>
</div>
`;
</script>
<Select
items={tags}
bind:value={$tag}
optionTemplate={tagRenderer}
placeholder="Select a tag..."
/>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 | | Svelte Integration | ⭐⭐⭐ Seamless | ⭐⭐⭐ Seamless | | Store Support | ⭐⭐⭐ Full | ⭐⭐⭐ Full | | 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
- Working with external APIs returning plain data
✅ 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
- Need to integrate with Svelte stores in rendering
Combining Both Methods
You can start with Method 1 and add Method 2 later as your UI evolves:
<script lang="ts">
import { Select } from '@smilodon/svelte';
let value = '';
// Start simple
const items = ['Option 1', 'Option 2', 'Option 3'];
// Later, add custom rendering without changing items
const customRenderer = (item: any, index: number) => `
<div style="padding: 8px; background: ${index % 2 ? '#f9fafb' : 'white'};">
<strong>${item.label || item}</strong>
</div>
`;
</script>
<Select
items={items}
bind:value
optionTemplate={customRenderer}
/>Performance Tips
For Method 1:
- Use reactive statements (
$:) to memoize large item arrays - Enable
virtualizedprop for 1000+ items - Enable
infiniteScrollfor dynamic loading
For Method 2:
- Keep renderer function pure (no side effects)
- Avoid heavy computations in renderer
- Cache renderer functions when possible
- Use template literals for cleaner HTML strings
<script lang="ts">
import { Select } from '@smilodon/svelte';
let value = '';
// Memoize items with reactive statement
$: items = Array.from({ length: 10000 }, (_, i) => ({
value: i.toString(),
label: `Item ${i + 1}`,
description: `Description for item ${i + 1}`
}));
// Pure renderer function
const renderer = (item: any, index: number) => `
<div>
<div style="font-weight: 600;">${item.label}</div>
<div style="font-size: 12px; color: #666;">${item.description}</div>
</div>
`;
</script>
<Select
items={items}
bind:value
optionTemplate={renderer}
virtualized
estimatedItemHeight={60}
/>Styling
The component uses CSS variables for easy 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;
}Important: Passing inline functions as renderers
If you define optionRenderer or customRenderer inline (like function inside template), it may cause unnecessary re-creation.
Highly recommended: For best performance and to avoid potential issues, always use stable function references:
<script>
let myRenderer = (item, index, helpers) => {
return document.createElement('div');
};
// or define outside reactive blocks
</script>Adapters are designed to not loop even without memoization, but stability still improves performance.
Accessibility
This component is fully accessible and compliant with WCAG 2.1 AAA standards:
- ✅ Keyboard navigation (Arrow keys, Enter, Escape, Tab)
- ✅ Screen reader support (ARIA labels and live regions)
- ✅ Focus management
- ✅ High contrast mode support
- ✅ Reduced motion support
Browser Support
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Opera 76+
License
MIT © Navid Rezadoost
Contributing
Contributions are welcome! Please see the main repository for guidelines.
