Web component

<p-select>

A select that can be searched, built as an editable combobox

Import
@parallelogram-js/core/components/PSelect
Tag
p-select

Playground

<form class="form" action="#chosen">
  <div class="form__group">
    <label class="form__label" for="dining-area">Dining area</label>
    <div class="form__control">
      <p-select id="dining-area" name="area" placeholder="Choose an area" required>
        <optgroup label="Inside">
          <option value="bar">Bar</option>
          <option value="main">Main room</option>
        </optgroup>
        <optgroup label="Outside">
          <option value="terrace" selected>Terrace</option>
          <option value="garden" disabled>Garden (closed for winter)</option>
        </optgroup>
      </p-select>
    </div>
  </div>
  <div class="form__actions">
    <button type="submit">Continue</button>
    <button type="reset">Reset</button>
  </div>
</form>

An option can carry more than its label: data-secondary is shown muted after the label and searched with it, so typing an email finds the person; data-description is a smaller line beneath; data-image is a round thumbnail before the text. The input shows the label alone once someone is chosen. Remote options carry the same fields as secondary, description and image.

<div class="form">
  <div class="form__group">
    <label class="form__label" for="owner">Owner</label>
    <div class="form__control">
      <p-select id="owner" name="owner" placeholder="Choose an owner">
        <option
          value="amelia"
          data-secondary="amelia@example.com"
          data-description="Account manager, Sydney"
          data-image="images/harbour-640.jpg"
        >
          Amelia Nguyen
        </option>
        <option
          value="hudson"
          data-secondary="hudson@example.com"
          data-description="Head chef, Melbourne"
          data-image="images/kitchen-640.jpg"
        >
          Hudson Ferraro
        </option>
        <option
          value="priya"
          data-secondary="priya@example.com"
          data-description="Events, Brisbane"
          data-image="images/terrace-640.jpg"
        >
          Priya Rahman
        </option>
        <option value="unassigned" data-description="Leave the record with no owner">Unassigned</option>
      </p-select>
    </div>
  </div>
</div>

With multiple the list toggles rather than replaces and stays open, and every chosen value sits in the control. A value goes back out by choosing its row again, or with Backspace from the empty input. Nothing is counted away behind "and 2 more": the field grows to about three rows and then scrolls. selection-rows="2" and list-rows="2" put the secondary text under the label in the control and in the list, and select-all adds the bar that takes or gives back whatever the search has narrowed to. The form carries one entry per value under the one name.

<div class="form">
  <div class="form__group">
    <label class="form__label" for="shift">Staff on shift</label>
    <div class="form__control">
      <p-select
        id="shift"
        name="staff"
        multiple
        select-all
        selection-rows="2"
        list-rows="2"
        placeholder="Nobody chosen yet"
      >
        <option value="amelia" data-secondary="Duty manager, front of house">Amelia Nguyen</option>
        <option value="hudson" data-secondary="Head chef, kitchen">Hudson Ferraro</option>
        <option value="priya" data-secondary="Coordinator, events">Priya Rahman</option>
        <option value="marcus" data-secondary="Supervisor, kitchen">Marcus Bell</option>
        <option value="ines" data-secondary="Technician, maintenance">Ines Duarte</option>
        <option value="rosa" data-secondary="Sommelier, front of house">Rosa Iglesias</option>
      </p-select>
    </div>
  </div>
</div>

Around a hundred options, loaded from a URL so the sample stays readable. The list scrolls once it outgrows its maximum height, typing narrows it and the arrow keys, Page Up and Page Down walk it. With data-select-min="0" the options arrive as soon as the list opens.

<div class="form">
  <div class="form__group">
    <label class="form__label" for="delivery-suburb">Delivery suburb</label>
    <div class="form__control">
      <p-select
        id="delivery-suburb"
        name="suburb"
        placeholder="Choose a suburb"
        data-select-src="/api/places?q={q}"
        data-select-min="0"
      ></p-select>
    </div>
  </div>
</div>

The search runs on the server: each query is sent to data-select-src with {q} replaced by the typed text, and the options that come back replace the list. The URL also carries {page} and {limit}, so the server sends a page at a time — 25 rows here, from data-select-limit — and answers more: true while there are further pages; scrolling to the end of the list, or arrowing onto its last row, asks for the next one. Each row carries an email as its secondary and a role as its description, so the list shows both and a search on nguyen, perth or an address like noah.tran finds its person. Type two characters to start a search, and the list shows its busy state while the request is out and "No results found" when nothing matches.

<div class="form">
  <div class="form__group">
    <label class="form__label" for="customer">Customer</label>
    <div class="form__control">
      <p-select
        id="customer"
        name="customer"
        placeholder="Search customers"
        data-select-src="/api/directory?q={q}&page={page}&limit={limit}"
        data-select-min="2"
        data-select-debounce="300"
      ></p-select>
    </div>
  </div>
</div>

About

Follows the WAI-ARIA combobox pattern with list autocomplete. The text input carries the combobox role and is named after the element's aria-label or its <label for>. Typing filters the options and announces how many match; the arrow keys, Home and End move through them, and Page Up and Page Down move ten at a time; Enter or Tab chooses the highlighted option; Alt+Down Arrow opens the list without moving the highlight and Alt+Up Arrow chooses the highlighted option and closes it; Escape closes the list and puts the chosen label back. Backspace or Delete clears a chosen value and returns the input to searching, which is the keyboard's way back: choosing a value makes the input read-only, and the clear button beside it is not in the tab order.

Options come from <option> and <optgroup> children, which are watched for changes, or from a URL that returns JSON: an array of { value, label, disabled?, group?, secondary?, description?, image? }, or an object with those in options. An option may carry a secondary text, shown muted after its label and searched along with it, a description, shown smaller beneath, and an image URL, shown as a round thumbnail before the text; in markup they are the data-secondary, data-description and data-image attributes of the <option>. The input shows the label alone. The element is form-associated: it submits its value under its name, supports required, and restores its value attribute, or else its selected option, when the form resets.

Without JavaScript

Until the module loads this element is empty markup with no control in it, and the package stylesheet hides it while scripts are running, so the page never shows a bare element nobody can use. It is the form field, so a form submitted before it upgrades carries no value for it and required cannot hold the form back. Where that matters, render an ordinary <select> on the server and replace it once the module is in.

Usage

Register the component and the framework loads it the first time a page contains p-select.

import { Parallelogram } from '@parallelogram-js/core';

const app = Parallelogram.create();
app.components.add('p-select', () => import('@parallelogram-js/core/components/PSelect'));
app.run();

Attributes

NameTypeDefaultDescription
namestringnoneThe form field name Property name.
valuestringnoneThe initial value, restored when the form resets; takes precedence over a selected option Property value.
multipleflagnoneAllows more than one value: choosing toggles rather than replaces, the field submits one entry per value under its name, and value reads and writes a list
selection-rows1 | 21How many rows a chosen value takes in the control: its label, or its label over the option's secondary text
list-rows1 | 21How many rows an option takes in the list: its label with the secondary text after it, or the secondary text on its own line beneath
searchableflagnoneOffers a box to type in that narrows the list. Without it a printable key moves the field to the option it names, as a native select does, which suits a handful of options where a search box is more furniture than help
search-labelstringSearchThe placeholder in the search box once something is chosen, when the field’s own placeholder has given way to the selections For every instance, PSelect.defaults.searchLabel.
search-clear-labelstringClear the searchThe accessible name of the button that takes back what was typed in the search box For every instance, PSelect.defaults.searchClearLabel.
select-allflagnoneOffers a bar above the list that takes or gives back everything the search has narrowed to; only where multiple is set
select-all-labelstringSelect all ({count})The words on the button that takes every option the search left; {count} is how many For every instance, PSelect.defaults.selectAllLabel.
select-none-labelstringNoneThe words on the button that gives every option the search left back For every instance, PSelect.defaults.selectNoneLabel.
placeholderstringSelect…Shown when nothing is chosen For every instance, PSelect.defaults.placeholder.
search-hintstringType to searchWhat the list says before a search with data-select-min of 1 has begun For every instance, PSelect.defaults.searchHint.
search-min-hintstringType {min} or more characters to searchThe same when data-select-min is higher, with {min} for it For every instance, PSelect.defaults.searchMinHint.
no-resultsstringNo results foundWhat the list says when nothing matches For every instance, PSelect.defaults.noResults.
disabledflagnoneDisables the select Property disabled.
requiredflagnoneA value must be chosen for the form to submit Property required.
aria-labelstringnoneNames the input when there is no <label for>
data-select-srcurlnoneURL for remote options, with {q} replaced by the search text. With {page} in it the list is paged: page 1 first, then the next page whenever the list is scrolled to its end or the arrow keys reach the last option, for as long as the response says more: true. {limit} is replaced by data-select-limit.
data-select-minnumber0Characters to type before a remote search
data-select-debouncenumber200Milliseconds to wait after typing before a remote search
data-select-limitnumber25Rows per page, replacing {limit} in data-select-src
data-select-open-on-focusbooleanfalseOpen the list when the input receives focus

Properties

NameTypeDescription
valuestringThe chosen value
namestringThe form field name
requiredbooleanReflects the required attribute
disabledbooleanReflects the disabled attribute
formread onlyHTMLFormElement | nullThe owning form
labelsread onlyNodeListLabels for the element
validityread onlyValidityStateThe validity state
validationMessageread onlystringThe validation message
willValidateread onlybooleanWhether the element is validated

Methods

NameSignatureDescription
open() => voidOpen the list
close() => voidClose the list
toggle() => voidOpen or close the list
select(value: string) => voidChoose an option as the user would, dispatching input, change and p-select:change when the value changes
setOptions(options: Array<{ value: string; label: string; disabled?: boolean; group?: string; secondary?: string; description?: string; image?: string }>) => voidReplace the options
getValue() => stringThe chosen value
clear() => voidClear the choice
refreshOptions() => voidRead the child options again
checkValidity() => booleanCheck the value
reportValidity() => booleanCheck the value and show the browser message

Events

NameDetailDescription
inputnoneThe user chose a different option; bubbles out of shadow roots DOM event
changenoneThe user chose a different option; bubbles out of shadow roots DOM event
p-select:change{ value: string; label: string; secondary?: string; description?: string; image?: string }A different option was chosen, with its secondary text, description and image when it has them; bubbles out of shadow roots DOM event
p-select:opennoneThe list opened DOM event
p-select:closenoneThe list closed DOM event

Shadow parts

NameDescription
controlThe field itself, which carries the combobox role and focus
menuThe popup, holding the search bar, the bulk bar and the list
searchThe search bar at the top of the popup, where searchable is set
inputThe search box inside that bar
search-clearThe button that takes back what was typed into it
selectionsThe chosen values in the control
selectionOne chosen value
bulkThe bar above the list, where select-all is set
bulk-allThe button that takes every option the search left
bulk-noneThe button that gives those options back
listboxThe list of options

CSS custom properties

NameDefaultDescription
--select-row1.75remHow tall one row of the control is, which is what keeps the icons in the gutter level with the first row as the field grows. The element measures a real row and writes it here once anything is chosen, so a page rarely sets it
--select-menu-max-height240pxHow tall the list grows before it scrolls; set it to none and the list fits what is in it
--select-secondary-font-sizevar(--font-xs)The size of an option's secondary text where list-rows is 2
--select-chosen-bgvar(--color-accent)Background of a chosen row in the list; it is filled, not tinted, whether one value may be chosen or several
--select-chosen-colorvar(--color-accent-contrast)Text colour of a chosen row in the list
--select-chosen-hover-bgvar(--color-accent-hover)Background of a chosen row under the pointer or the keyboard
--select-selections-max-height7.2remHow tall the chosen values grow in the control before they scroll
--select-selection-bgvar(--select-selected-bg)Background of one chosen value; the same tint a chosen option carries
--select-selection-radius999pxCorner radius of one chosen value
--select-selection-max-width16remHow wide one chosen value grows before its label is cut short
--select-selection-font-sizevar(--font-sm)The size of a chosen value's label
--select-selection-secondary-font-sizevar(--font-xs)The size of a chosen value's second line, where selection-rows is 2
--select-bgvar(--surface-control-color-bg)Background of the closed control; follows the control surface
--select-bordervar(--select-border-width) solid var(--select-border-color)The control's border, built from its width and colour
--select-border-widthvar(--surface-control-border-width)Width of that border; follows the control surface
--select-border-radiusvar(--surface-control-radius)Corner radius of the control; follows the control surface
--select-padding-block0.45remPadding above and below the control’s rows, which the icons in the gutter start from
--select-padding-inline0.8remPadding at the leading and trailing edges of the control, which the chevron sits in from
--select-selection-padding-inline0.6remA chosen value's own inset. The control gives back exactly this much of its leading padding, so a value's text starts in the same place whether it is drawn on a ground of its own or not
--select-guttercalc(var(--select-padding-inline) + 2.85rem)The trailing strip the control keeps for the chevron and the slot beside it, whether or not anything is drawn there, so opening the list never moves what is already in the field
--select-placeholdervar(--color-text-muted)Colour of the placeholder while nothing is chosen
--select-active-outlinevar(--color-accent-hover)Outline drawn around the control while its list is open
--select-textvar(--surface-control-color-text)Text colour
--select-border-colorvar(--surface-control-border-color)Input border colour
--select-focus-colorvar(--color-accent)Focus ring colour
--select-hover-bgvar(--surface-dropdown-item-hover-bg)Background of a hovered option
--select-current-bgvar(--surface-dropdown-item-current-bg)Background of the highlighted option
--select-selected-bgvar(--surface-dropdown-item-selected-bg)Background of the chosen option
--select-description-colorvar(--color-text-muted)Colour of an option's description
--select-image-size1.5remWidth and height of an option's thumbnail
--select-disabled-opacityvar(--form-control-disabled-opacity, 0.3)Opacity while the select is disabled
--menu-bgvar(--surface-dropdown-color-bg)List background
--menu-border-colorvar(--surface-dropdown-border-color)List border colour
--menu-border-radiusnoneList corner radius
--menu-shadowvar(--surface-dropdown-shadow)List shadow