@polarityio/pi-select
v1.3.1
Published
Feature-rich select/dropdown with single and multi-select, async search, and custom rendering
Readme
Polarity Integration Component Library
Select Component
A feature-rich select/dropdown component supporting single and multi-select modes, synchronous and asynchronous search, custom option rendering, and intelligent dropdown positioning via Floating UI.
Installation
npm install @polarityio/pi-selectPeer Dependencies
- lit ^3.0.0
Usage
import '@polarityio/pi-select';Basic Single Select
import { html } from 'lit';
html`
<pi-select placeholder="Choose a fruit" .options=${['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry']}></pi-select>
`;Multi-Select
import { html } from 'lit';
html`
<pi-select
multiple
placeholder="Select tags"
.options=${['Bug', 'Feature', 'Enhancement', 'Documentation']}
.selected=${['Bug', 'Feature']}
></pi-select>
`;Error State
import { html } from 'lit';
html`
<pi-select
label="Role"
placeholder="Select a role"
error
error-message="Please select a role"
.options=${['Admin', 'User', 'Moderator']}
></pi-select>
`;Async Search
import { html } from 'lit';
html`
<pi-select
placeholder="Search users..."
search-placeholder="Type a name..."
search-message="Type to search for users"
loading-message="Searching..."
.search=${async (term) => {
const res = await fetch(`/api/users?q=${term}`);
return res.json();
}}
label-field="name"
search-field="name"
></pi-select>
`;Custom Option Rendering
import { html } from 'lit';
const select = document.querySelector('pi-select');
select.options = [
{ name: 'Alice', role: 'Admin', avatar: '/alice.png' },
{ name: 'Bob', role: 'Editor', avatar: '/bob.png' }
];
select.labelField = 'name';
select.searchField = 'name';
select.renderOption = (option) => html`
<div style="display: flex; align-items: center; gap: 8px;">
<img src="${option.avatar}" width="24" height="24" style="border-radius: 50%;" />
<div>
<div>${option.name}</div>
<small style="opacity: 0.7;">${option.role}</small>
</div>
</div>
`;Handling Events
const select = document.querySelector('pi-select');
select.addEventListener('pi-change', (e) => {
console.log('Selected:', e.detail.selected);
console.log('Previous:', e.detail.previous);
});
select.addEventListener('pi-search', (e) => {
console.log('Search term:', e.detail.term);
console.log('Results:', e.detail.results);
});
select.addEventListener('pi-open', () => console.log('Dropdown opened'));
select.addEventListener('pi-close', () => console.log('Dropdown closed'));API Reference
Properties
| Property | Type | Default | Description |
| -------------------- | ----------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------- |
| name | string | '' | Name used for form submission via FormData when inside a <form> |
| label | string | '' | Accessible/visible label. Not rendered by default; when set, renders above the trigger |
| options | T[] | [] | Array of option items to display in the dropdown |
| selected | T \| T[] \| undefined | undefined | Currently selected option(s). Array for multiple mode. |
| multiple | boolean | false | Enable multi-select mode |
| placeholder | string | '' | Placeholder text when no option is selected |
| disabled | boolean | false | Disable the entire select component |
| error | boolean | false | Sets error state styling on the trigger |
| errorMessage | string | '' | Error message displayed below the trigger. Setting this also activates the error state. |
| noErrorSpace | boolean | false | When true, no space is reserved below the trigger when idle. The error message still appears when error-message is set. Useful for compact layouts. |
| searchEnabled | boolean | true | Enable search/filter functionality |
| searchField | string \| undefined | undefined | Object property name to search within (for object options) |
| labelField | string \| undefined | undefined | Object property name to display as the option label |
| allowClear | boolean | false | Show a clear button to reset the selection |
| closeOnSelect | boolean \| undefined | undefined | Close dropdown after selection. Defaults to true in single mode, false in multiple mode. |
| searchPlaceholder | string | '' | Placeholder text for the search input |
| noMatchesMessage | string | 'No results found' | Message displayed when no options match the search |
| searchMessage | string | 'Type to search' | Message shown in async search mode before the first search |
| loadingMessage | string | 'Loading...' | Message displayed while loading async search results |
| highlightOnHover | boolean | true | Highlight options on mouse hover |
| matchTriggerWidth | boolean | true | Make dropdown width match the trigger width |
| triggerClass | string | '' | Additional CSS class(es) for the trigger element |
| dropdownClass | string | '' | Additional CSS class(es) for the dropdown element |
| triggerTabindex | number | 0 | Tab index for keyboard navigation |
| searchDebounce | number | 150 | Debounce delay in milliseconds for search filtering |
| loading | boolean | false | Indicates async search is in progress |
| search | (term: string) => Promise<T[]> | undefined | Async search function. When provided, enables async search mode. |
| matcher | (option: T, term: string) => number | undefined | Custom matcher function for filtering. Return -1 for no match, 0+ for match position. |
| renderOption | (option: T) => TemplateResult \| string | undefined | Custom render callback for option display in the dropdown |
| renderSelectedItem | (option: T) => TemplateResult \| string | undefined | Custom render callback for selected item display in the trigger |
Form participation
pi-select is a form-associated custom element: when placed inside a <form>, its name/selected are included in the form's FormData on submit. For string/number options, the submitted value is the option itself. For object options, there's no dedicated "value" concept distinct from the display label, so it falls back to the same label labelField produces — set labelField for a meaningful submitted value, or handle pi-change and manage submission yourself if you need something else. In multiple mode, one FormData entry is appended per selected item (formData.getAll(name)), mirroring a native <select multiple>.
Set label for a visible label above the trigger (referenced via aria-labelledby since a role="combobox" element isn't a labelable target for <label for>), or set aria-label/aria-labelledby on the host directly for a label-less accessible name. Neither is rendered unless explicitly set, so existing usages are unaffected.
Events
| Event Name | Detail | Description |
| ----------- | ---------------------------------------------------------------------- | -------------------------------------------------------- |
| pi-change | { selected: T \| T[] \| undefined, previous: T \| T[] \| undefined } | Fired when the selection changes |
| pi-open | {} | Fired when the dropdown opens |
| pi-close | {} | Fired when the dropdown closes |
| pi-search | { term: string, results: T[] } | Fired on search input with the term and filtered results |
| pi-focus | {} | Fired when the select gains focus |
| pi-blur | {} | Fired when the select loses focus |
Slots
| Slot Name | Description |
| ---------------- | --------------------------------------------------------------------- |
| before-options | Content displayed before the options list in the dropdown |
| after-options | Content displayed after the options list in the dropdown |
| loading | Custom loading message (replaces loadingMessage when provided) |
| no-matches | Custom no-matches message (replaces noMatchesMessage when provided) |
CSS Custom Properties
| Property | Default | Description |
| ---------------------------------------- | ----------------------------------------------- | -------------------------------------------------- |
| --pi-select-dropdown-z-index | 1000 | Dropdown z-index |
| --pi-select-line-height | 1.75em | Select line height |
| --pi-select-trigger-padding | 4px 16px 4px 8px | Trigger padding |
| --pi-select-border-color | var(--pi-color-border-element) | Border color |
| --pi-select-border-radius | var(--pi-size-radius-base) | Border radius |
| --pi-select-background | var(--pi-color-background-container-base) | Background color |
| --pi-select-text-color | var(--pi-color-font-primary) | Text color |
| --pi-select-disabled-bg | var(--pi-color-background-container-disabled) | Disabled background |
| --pi-select-placeholder-color | var(--pi-color-font-disabled) | Placeholder color |
| --pi-select-loading-icon-color | var(--pi-color-font-disabled) | Loading icon color |
| --pi-select-dropdown-border | 1px solid var(--pi-select-border-color) | Dropdown border |
| --pi-select-dropdown-box-shadow | var(--pi-shadow-sm) | Dropdown shadow |
| --pi-select-dropdown-margin | 2px 0 0 | Dropdown margin from trigger |
| --pi-select-option-padding | 4px 8px | Option padding |
| --pi-select-search-input-border-radius | 3px | Search input border radius |
| --pi-select-search-field-border | 1px solid var(--pi-select-border-color) | Search field border |
| --pi-select-visible-options | 7 | Maximum number of visible options before scrolling |
| --pi-select-highlight-bg | var(--pi-color-background-primary) | Highlighted option background |
| --pi-select-highlight-color | var(--pi-color-font-inverse) | Highlighted option text color |
| --pi-select-selected-bg | var(--pi-color-background-hover) | Selected option background |
| --pi-select-disabled-color | var(--pi-color-font-disabled) | Disabled option text color |
| --pi-select-tag-bg | var(--pi-color-background-secondary) | Multi-select tag background |
| --pi-select-tag-color | var(--pi-color-font-primary) | Multi-select tag text color |
| --pi-select-tag-border | 1px solid var(--pi-color-border-container) | Multi-select tag border |
| --pi-select-tag-border-radius | 3px | Multi-select tag border radius |
| --pi-select-tag-padding | 0 4px | Multi-select tag padding |
CSS Parts
| Part Name | Description |
| -------------------- | ----------------------------------------- |
| trigger | The select trigger/button area |
| selected-item | Selected item text (single mode) |
| placeholder | Placeholder text |
| clear-btn | Clear button |
| loading-icon | Loading spinner icon |
| status-icon | Dropdown arrow icon |
| tags-area | Area containing tags (multiple mode) |
| tag | Individual tag (multiple mode) |
| tag-label | Tag label text (multiple mode) |
| tag-remove | Tag remove button (multiple mode) |
| search-input | Search input field |
| icons | Icons container (multiple mode) |
| dropdown | Dropdown container |
| search | Search input container (single mode) |
| options-list | Options list container (role="listbox") |
| option | Individual option |
| option-highlighted | Highlighted option (keyboard/hover) |
| option-selected | Selected option |
| option-disabled | Disabled option |
| no-matches | No-matches message element |
| loading | Loading message element |
| error-message | The error message container below the trigger |
Public Methods
| Method | Description |
| ---------- | ------------------------------------------------------ |
| open() | Opens the dropdown. No-op if already open or disabled. |
| close() | Closes the dropdown and returns focus to the trigger. |
| toggle() | Toggles the dropdown between open and closed states. |
Keyboard Navigation
| Key | Behavior |
| ----------------- | ------------------------------------------------------ |
| ArrowDown | Open dropdown or move highlight to the next option |
| ArrowUp | Open dropdown or move highlight to the previous option |
| Home | Move highlight to the first option |
| End | Move highlight to the last option |
| Enter / Space | Select highlighted option or open dropdown |
| Escape | Close dropdown |
| Tab | Close dropdown and move focus |
| Backspace | Remove last selected tag (multiple mode, empty search) |
| (type-ahead) | Jump to matching option when dropdown is closed |
Theming
Customize the appearance using CSS custom properties. This component uses design tokens from @polarityio/pi-shared for consistent theming across the component library.
pi-select {
--pi-select-border-radius: 8px;
--pi-select-visible-options: 10;
--pi-select-highlight-bg: #3b82f6;
}
pi-select::part(trigger) {
min-height: 40px;
}
pi-select::part(tag) {
border-radius: 12px;
}License
MIT
