npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@smilodon/core

v1.5.4

Published

High-performance native select component with extreme-scale virtualization - React, Vue, Svelte, Vanilla JS

Readme

@smilodon/core

📖 Documentation

For comprehensive documentation covering all features, styling options, and advanced patterns:

👉 Complete Vanilla JS Guide 👈

Additional references:

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/core

Quick 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 to SelectItem format
  • Number arrays: [1, 2, 3, 5, 8] - automatically converted to SelectItem format

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-color

Dark 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 selection
  • Ctrl/Cmd + Click - Toggle individual items
  • Shift + 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 needed
  • horizontal - Single row, horizontal scroll, drag-to-scroll enabled
  • vertical - Single column, vertical scroll only
  • both - 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:

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 select
  • top — always above the select
  • auto — 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:

  • badge
  • badgeHover
  • badgeActive
  • badgeLabel
  • badgeRemove
  • badgeRemoveHover
  • badgeRemoveActive
  • groupHeader
  • activeOption

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:

  1. Virtual Scrolling: Only renders visible items (10-15 DOM nodes for any dataset size)
  2. Work Stealing Scheduler: Search operations run in background workers with automatic cancellation
  3. 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

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