Enterprise-grade, high-performance select components engineered for extreme data scale, accessibility, and compliance.
Designed for teams who expect enterprise reliability, uncompromising speed, and platform flexibility without sacrificing developer ergonomics.
Latest (v1.4.12): Added runtime capability reporting, known limitation policies, tracking snapshots, and diagnostic eventing in core; exposed these controls across React/Vue/Svelte/Vanilla adapters with new contract + unit coverage and Playwright preflight checks.
Notable changes in v1.4.8
groupedItems or a flat items array where each item has a group property — adapters auto-convert when possible (React, Vue, Svelte)..dark, .dark-mode) or data-theme="dark". These are applied inside Shadow DOM using :host-context, and are configurable with CSS variables described in packages/core/src/components/enhanced-select.ts.Smilodon is a Web Component powered select/dropdown system that remains responsive even when navigating millions of records. It exposes consistent ergonomics across React, Vue, Svelte, and Vanilla JavaScript while keeping the core bundle at 6.6 KB gzipped.
| Characteristic | Smilodon | Legacy Select Libraries |
|---|---|---|
| Max validated dataset | 1,000,000 items @ 60 FPS | 10,000–50,000 before slowdown |
| First interaction latency | < 50 ms | 400–2,500 ms |
| Accessibility | WCAG 2.2 AA/AAA (in progress) | Partial ARIA coverage |
| Security | CSP safe, zero eval, SBOM shipped | Mixed CSP compliance |
Why it matters: teams can standardize on a single select primitive that scales from onboarding forms to compliance dashboards without bespoke tuning.
performance.mark/measure), console timelines, and optional web worker profiling.packages/*) with typed exports and dual ESM/CJS output.Deep dives: ARCHITECTURE.md, docs/SELECT-IMPLEMENTATION.md, docs/ALGORITHMS.md.
| Capability | Highlights |
|---|---|
| Search modes | Client-side fuzzy, server-side async with debouncing, highlight rendering |
| Selection | Single, multi, tag-style chips, keyboard-only workflows |
| Data scale | 1M rows validated, configurable buffering, streaming hydration |
| Accessibility | Full ARIA pattern, screen reader announcements, focus trapping, 44px touch targets |
| Custom UI | Slot templates, theme tokens, light/dark theming, design-token aware |
| Custom Components | Pass framework components (React/Vue/Svelte) for rendering options with lifecycle management |
| Runtime control | getCapabilities(), limitation policy controls, and tracking snapshots across all adapters |
| Observability | Perf heat-map overlay (playground), console instrumentation, metrics exporter |
performance.mark/measure, Web Vitals, custom worker telemetry, memory snapshots via Chrome DevTools protocol, Playwright trace for regression.Dataset Size │ First Paint │ Interactive │ Search (p95) │ Memory │ Scroll FPS
────────────────┼─────────────┼─────────────┼──────────────┼────────┼───────────
100 items │ 7 ms │ 9 ms │ 3 ms │ 2 MB │ 60 FPS
1,000 items │ 14 ms │ 18 ms │ 5 ms │ 4 MB │ 60 FPS
10,000 items │ 38 ms │ 42 ms │ 9 ms │ 8 MB │ 60 FPS
100,000 items │ 81 ms │ 95 ms │ 16 ms │ 12 MB │ 60 FPS
1,000,000 items │ 162 ms │ 191 ms │ 33 ms │ 18 MB │ 57–60 FPS
Budget guardrails: Components yellow-flag at 50 ms render or 20 ms search time, red-flag above 100 ms/50 ms respectively. CI profiles enforce these limits per commit via
scripts/perf.js.
| Library | 10K dataset | 100K dataset | 1M dataset | Notes |
|---|---|---|---|---|
| Smilodon | 42 ms | 95 ms | 191 ms | Virtual viewport + worker search |
| React Select | 2,500 ms | ⚠️ Crash | - | DOM bloat, no virtualization |
| Downshift | 1,800 ms | 45,000 ms | ⚠️ Crash | CPU-bound filtering |
| Vue Select | 1,600 ms | 44,000 ms | ⚠️ Crash | Template re-render storm |
window.__SMILODON_PERF__ exposes counters for dashboards; see docs/PERFORMANCE.md for exporter schema.| Ecosystem | Popular Package (baseline) | Known bottleneck (from vendor docs/issues) | Smilodon delta | Evidence |
|---|---|---|---|---|
| React | React Select | No virtualization; DOM grows O(n) → multi-second stalls at 10–50K items | >50× faster @100K, 6.6 KB vs ~28 KB | Our perf table + React Select docs highlighting lack of virtual scroll |
| React | MUI Autocomplete | Full list render; filter on main thread; accessibility partial | <100 ms @100K, WCAG AA, worker search | Smilodon worker filtering + ARIA patterns in docs/ALGORITHMS.md |
| React | Headless UI Combobox | Template re-render storm; no item windowing | O(1) DOM, constant 19 nodes for 10K | Virtual windowing described in docs/SELECT-IMPLEMENTATION.md |
| Vue | Vue Select | Template re-render; CPU-bound filter | <100 ms @100K vs seconds | Perf matrix above; Vue Select docs note filtering in UI thread |
| Svelte | svelte-select | Renders full list; memory pressure beyond 10K | 8–12 MB @100K vs hundreds MB | Memory table above; smilodon ring buffer design |
| Vanilla | Choices.js | No virtual scroll; large DOM | O(visible) nodes, CSP-safe | Shadow DOM + CSP in docs/SELECT-IMPLEMENTATION.md |
Why Smilodon wins:
scripts/perf.js traces checked into CI.eval, shadow DOM isolation — contrasted with gaps in competing packages’ own docs.Each playbook contains installation, configuration, and a realistic example showcasing search, multi-select, keyboard control, and async data.
For comprehensive, unambiguous documentation covering all features, styling options, and framework-specific patterns:
Each guide includes:
npm install @smilodon/react
import { useMemo, useRef, useState } from 'react';
import { Select } from '@smilodon/react';
const products = Array.from({ length: 5000 }).map((_, i) => ({
value: `product-${i}`,
label: `Product ${i}`,
tags: i % 2 ? ['hardware'] : ['software']
}));
export function ProductSelect() {
const selectRef = useRef(null);
const [selection, setSelection] = useState([]);
const items = useMemo(() => products, []);
return (
<Select
ref={selectRef}
items={items}
searchable
multiSelect
placeholder="Search 5K products"
onSearch={(term) => console.log('debounced server query', term)}
onChange={(selectedItems, values) => setSelection(values)}
config={{
selection: { mode: 'multi', showChips: true },
keyboard: { loop: true, typeAhead: true },
metrics: { enabled: true }
}}
/>
);
}
Highlights: supports Suspense/SSR, controlled mode via refs (selectRef.current.setSelectedValues), compatibility with React 18 concurrent rendering.
npm install @smilodon/vue
<template>
<Select
:items="cities"
searchable
multi-select
placeholder="Filter cities"
@search="handleSearch"
@change="handleChange"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Select } from '@smilodon/vue';
const cities = ref([
{ value: 'nyc', label: 'New York City' },
{ value: 'ams', label: 'Amsterdam' },
{ value: 'tok', label: 'Tokyo' }
]);
const handleSearch = (term: string) => {
// plug into Pinia/Query clients for server filtering
console.log('search', term);
};
const handleChange = (_event, payload) => {
console.log('values', payload.selectedValues);
};
</script>
npm install @smilodon/svelte
<script lang="ts">
import Select from '@smilodon/svelte';
import { writable } from 'svelte/store';
const items = writable([]);
const selected = writable([]);
onMount(async () => {
const response = await fetch('/api/countries');
items.set(await response.json());
});
</script>
<Select
{items}
bind:selectedValues={$selected}
searchable
multiSelect
placeholder="Choose countries"
/>
npm install @smilodon/core
<enhanced-select id="people" placeholder="Search directory"></enhanced-select>
<script type="module">
import '@smilodon/core';
const select = document.getElementById('people');
select.setItems(await (await fetch('/directory.json')).json());
select.configure({ searchable: true, selection: { mode: 'multi' } });
select.addEventListener('change', (event) => {
console.log(event.detail.selectedItems);
});
</script>
Visit docs/GETTING-STARTED.md for additional platform nuances, SSR guidance, and theming recipes.
The following practical examples show non-destructive ways to style options so authors retain full control: via the JSON style on an item, via className + ::part() selectors, and via an optionRenderer that applies inline/background-image safely. These examples are also included in docs/STYLING-EXAMPLES.md for more detail.
style (per-option inline styles)<script>
const items = [
{
value: 'gradient',
label: 'Gradient option',
// Inline styles applied directly to the option container
style: {
backgroundImage: 'linear-gradient(180deg,#f4f4f4 0%, #d7d7d7 100%)',
color: '#111',
backgroundSize: 'cover'
}
},
{
value: 'plain',
label: 'Plain option'
}
];
const select = document.querySelector('enhanced-select');
select.setItems(items);
</script>
Notes:
style via Object.assign(this._container.style, style) so authors can set any valid CSS property.background shorthand to ensure proper overrides, if you want a background-image to persist on hover you can set a custom hover variable or use a custom class with ::part() rules.className + ::part() selectors (recommended for maintainable CSS)<style>
/* Target the internal option container via the part API */
enhanced-select::part(option).user-card {
display: flex;
gap: 12px;
padding: 12px;
align-items: center;
background-image: url('/avatars/alex.jpg');
background-size: cover;
color: white;
}
/* Hover state that preserves background-image by using background-image instead of background */
enhanced-select::part(option).user-card:hover {
filter: brightness(0.92);
}
/* Selected state via part selector */
enhanced-select::part(option).user-card.selected {
outline: 2px solid rgba(255,255,255,0.6);
}
</style>
<script>
const items = [
{ value: 1, label: 'Alex', className: 'user-card' },
];
document.querySelector('enhanced-select').setItems(items);
</script>
Why use ::part():
::part(option) targets the internal option container (part="option") and lets authors write external CSS to style options consistently.className together with ::part() keeps markup clean and avoids inline style repetition.If you need rich markup per option, return a DOM node (or HTML string) from your option renderer. To avoid your background-image being unintentionally removed by the component's hover background shorthand, use a CSS variable for hover/selected tokens or handle the hover inside your renderer's markup using a nested element.
function optionRenderer(item, index) {
// Create a wrapper that the component will place inside the option container
const wrapper = document.createElement('div');
wrapper.className = 'option-renderer-inner';
wrapper.style.backgroundImage = `url(${item.image})`;
wrapper.style.backgroundSize = 'cover';
wrapper.style.padding = '12px';
wrapper.style.color = item.textColor || '#fff';
wrapper.innerHTML = `
<div class="title">${item.label}</div>
<div class="subtitle">${item.subtitle || ''}</div>
`;
return wrapper; // returns HTMLElement -> mounted into option container
}
const items = [
{ value: 1, label: 'Photo', image: '/img/1.jpg', render: optionRenderer }
];
document.querySelector('enhanced-select').setItems(items);
Tips to preserve images and custom styling:
.option-renderer-inner) rather than relying solely on the option container's background, so component-level background rules won't remove your nested element's background-image.--select-option-hover-bg: transparent (or an explicit color) for the select instance to avoid hover clearing your image, e.g.: element.style.setProperty('--select-option-hover-bg','transparent').part for theming (dark/light): set enhanced-select::part(option){ --my-token: ... } and reference those tokens in your global CSS.className and JSON style for small per-item overrides (e.g., badge color) while keeping layout CSS in a stylesheet.render and className to make the renderer provide structure while the stylesheet controls visual polish.These examples give you maximal flexibility while preserving predictable hover/selected behavior from the component. See docs/STYLING-EXAMPLES.md for more detail and runnable snippets.
defaultValue is initialized once.packages/react/tests/infinite-render.spec.tsx and are kept green in CI/local checks.act(...) warning noise is filtered in React test config only, with no production/runtime impact.| Suite | Status | Notes |
|---|---|---|
| Unit (Vitest) | 76 / 76 ✅ | Snapshot + behavior tests |
| Integration | 45 / 45 ✅ | Focused on config combinations |
| E2E (Playwright) | 17 / 22 ⚙️ | Remaining scenarios tracked in tests/e2e/scenarios |
| Accessibility | 32 / 32 ✅ | axe, Lighthouse, Pa11y, screen reader manual passes |
| Performance | 18 / 18 ✅ | npm run perf enforcing latency budgets |
Run locally:
npx playwright install --with-deps
npm run test:unit
npm run test:contracts
npm run test:e2e
npm run test:coverage
npm run perf
Reference documents: TESTING-GUIDE.md, tests/README.md, and SETUP.md.
docs/compliance/SOC2-COMPLIANCE.md.docs/compliance/WCAG-COMPLIANCE.md.docs/compliance/PRIVACY-POLICY.md.sbom.json), SLSA-ready build steps, signed packages, CSP-friendly runtime.docs/compliance/THREAT-MODEL.md./playground Vite workspace with React, Svelte, Vue demos plus performance overlays.npm publish ready packages inside packages/* with semantic versioning.