Rich Search Input With Token Highlighting – rich-input

Category: Form , Javascript | September 17, 2026
Authorbramus
Last UpdateSeptember 17, 2026
LicenseMIT
Views0 views
Rich Search Input With Token Highlighting – rich-input

rich-input is a dependency-free Web Component for search fields that mix free text with structured keyword:value filters.

It autocompletes both filter names and their allowed values, highlights recognized filters inside the field, and uses nested <datalist> elements for configuration.

Rich option markup can display logos or avatars in suggestion rows, and the component participates in standard form submission.

Shadow Parts and slots expose the control for CSS and custom content, while getParsedQuery() splits the current value into free text and structured filters.

Features

  • Free text and structured keyword:value filters in one search field.
  • Autocomplete for both filter names and filter values, with matching based on the complete active token.
  • Declarative <datalist> configuration with runtime updates.
  • Automatic quotes for selected values that contain spaces, plus highlighting for invalid keywords and values.
  • Images, icons, and other option markup inside suggestion rows.
  • Native form submission, FormData, and form reset behavior.
  • In-input filter styling through the CSS Custom Highlight API.
  • Shadow Parts and leading/trailing slots for control customization.
  • ARIA combobox states and keyboard controls for Arrow keys, Enter, Tab, and Escape.

How To Use It

Installation

Load the ES module directly in the HTML document:

<script
  type="module"
  src="https://esm.sh/rich-input/es2022/rich-input.bundle.mjs">
</script>

For npm projects:

npm install rich-input

Import the package from your app entry file:

import 'rich-input';

Basic Usage

Place one or more <datalist> elements inside <rich-input>. Each datalist ID becomes a filter keyword.

<rich-input
  name="q"
  placeholder="Search products..."
>
  <datalist id="brand" label="Brand">
    <option value="Acme"></option>
    <option value="Northstar Audio"></option>
    <option value="Pixel Works"></option>
  </datalist>
  <datalist id="year" label="Release Year" data-type="number">
    <option value="2026"></option>
    <option value="2025"></option>
    <option value="2024"></option>
  </datalist>
  <datalist id="category" label="Category">
    <option value="Headphones"></option>
    <option value="Portable Audio"></option>
    <option value="Speakers"></option>
  </datalist>
</rich-input>

Typing brand: opens the values configured for that filter. Free text can appear beside structured filters:

wireless brand:"Northstar Audio" year:2026

Datalist Configuration

Element / AttributeTypeDescription
<datalist id="...">stringRequired keyword identifier. id="brand" creates the brand: filter. Matching is case-insensitive.
<datalist label="...">stringHuman-readable label shown in the suggestions panel. Defaults to a capitalized form of the ID.
<datalist data-type="...">stringSets the value type to string or number. The default is string.
<option value="...">stringValue inserted into the query. Values containing spaces receive quotes automatically.
<option label="...">stringOptional descriptive text for the option.
<option> childrenNodeOptional custom markup such as an image or icon rendered with the suggestion.

Rich Suggestions With Images

Child markup inside an <option> appears in the suggestion row. Selecting the suggestion inserts the option’s value into the search field.

<rich-input placeholder="Search catalog...">
  <datalist id="brand" label="Brand">
    <option value="Northstar Audio">
      <img
        src="northstar.svg"
        width="40"
        height="40"
        alt="Northstar Audio"
      >
      Northstar Audio
    </option>
    <option value="Pixel Works">
      <img
        src="pixel-works.svg"
        width="40"
        height="40"
        alt="Pixel Works"
      >
      Pixel Works
    </option>
  </datalist>
</rich-input>

Style Keyword Highlights

Each configured keyword can have its own ::highlight() rule. rich-input-keyword styles filter prefixes, and rich-input-invalid marks unrecognized filters or values.

The Custom Highlight API accepts text-oriented properties such as color, background-color, text-decoration, text-shadow, -webkit-text-stroke-color, -webkit-text-stroke-width, and -webkit-text-fill-color.

-:highlight(brand) {
  background-color: #dbeafe;
  color: #1e40af;
}
-:highlight(year) {
  background-color: #fef3c7;
  color: #92400e;
}
-:highlight(rich-input-keyword) {
  color: #64748b;
}
-:highlight(rich-input-invalid) {
  text-decoration: underline wavy #dc2626;
  text-decoration-skip-ink: none;
}

Instance-specific Highlight Styles

Place a <style> element directly inside <rich-input> when one instance needs its own highlight rules.

<rich-input
  value='brand:"Northstar Audio"'
  placeholder="Search products..."
>
  <style>
-:highlight(brand) {
      background-color: #dbeafe;
      color: #1e40af;
    }
  </style>
  <datalist id="brand" label="Brand">
    <option value="Northstar Audio"></option>
    <option value="Pixel Works"></option>
  </datalist>
</rich-input>

Style the Component With Shadow Parts

rich-input::part(control) {
  border: 2px solid #2563eb;
  border-radius: 9999px;
  padding: 0 1rem;
}
rich-input::part(input) {
  font-family: monospace;
  font-size: 1rem;
}
rich-input::part(popover) {
  border-radius: 12px;
  box-shadow: 0 12px 30px rgb(0 0 0 / 0.15);
}
rich-input::part(suggestion-item-active) {
  background-color: #dbeafe;
}
rich-input::part(suggestion-image) {
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  object-fit: cover;
}

Available Shadow Parts

Shadow PartDescription
::part(control)Outer control containing the icon, input, and clear button.
::part(input)Internal text input.
::part(icon)Default search icon.
::part(clear-button)Clear button displayed when text is present.
::part(popover)Autocomplete suggestions panel.
::part(suggestions-header)Header at the top of the suggestions panel.
::part(suggestions-list)<ul> containing the suggestions.
::part(suggestion-item)Individual suggestion row.
::part(suggestion-item-active)Focused or hovered suggestion row.
::part(suggestion-item-selected)Suggestion matching the current input value.
::part(suggestion-keyword)Keyword text inside a suggestion.
::part(suggestion-value)Value text inside a suggestion.
::part(suggestion-content)Content wrapper inside a suggestion row.
::part(suggestion-image)Image or icon rendered inside a rich suggestion.

Slots

Use slot="leading" to replace the default search icon. slot="trailing" places custom content after the clear button.

<rich-input placeholder="Search products...">
  <svg
    slot="leading"
    width="18"
    height="18"
    viewBox="0 0 24 24"
    aria-hidden="true"
  >
    <!-- icon paths -->
  </svg>
  <datalist id="category" label="Category">
    <option value="Audio"></option>
    <option value="Computers"></option>
  </datalist>
  <kbd slot="trailing">/</kbd>
</rich-input>
SlotDescription
leadingCustom leading icon or content. The default search icon acts as fallback content.
trailingCustom content rendered after the clear button.
Default slotHolds <datalist> configuration elements.

Form Integration

Set name to include the current rich-input value in standard form submission and FormData.

<form id="product-search" action="/search" method="GET">
  <rich-input name="q" placeholder="Search products...">
    <datalist id="category" label="Category">
      <option value="Audio"></option>
      <option value="Computers"></option>
    </datalist>
  </rich-input>
  <button type="submit">Search</button>
</form>
<script>
const form = document.querySelector('#product-search');
form.addEventListener('submit', function (event) {
  event.preventDefault();
  const data = new FormData(form);
  console.log(data.get('q'));
});
</script>

Parse the Current Query

getParsedQuery() separates ordinary search text from recognized filters.

const input = document.querySelector('rich-input');
input.value =
  'wireless brand:"Northstar Audio" year:2026';
console.log(input.getParsedQuery());

The result uses this structure:

{
  raw: 'wireless brand:"Northstar Audio" year:2026',
  text: 'wireless',
  keywords: {
    brand: ['Northstar Audio'],
    year: ['2026']
  },
  tokens: [
    // parsed token data
  ]
}

Update Datalists at Runtime

rich-input watches its slotted configuration for changes. Insert, remove, or update datalists after initialization to refresh the available filters and suggestions.

const input = document.querySelector('rich-input');
const datalist = document.createElement('datalist');
datalist.id = 'rating';
datalist.label = 'Rating';
for (const value of ['5', '4', '3']) {
  const option = document.createElement('option');
  option.value = value;
  datalist.append(option);
}
input.append(datalist);

JavaScript API

Properties

PropertyTypeDescription
valuestringGets or sets the current input value and refreshes highlights and form data.
placeholderstringGets or sets the placeholder text.
disabledbooleanEnables or disables the control.
namestringSets the form field name.
selectionStartnumberGets the start index of the current selection or caret.
selectionEndnumberGets the end index of the current selection or caret.

Methods

MethodDescription
getParsedQuery()Returns { raw, text, keywords, tokens } for the current query.
getKeywords()Returns the configured datalist keyword definitions and options.
focus(options)Focuses the internal input.
blur()Removes focus from the internal input.
select()Selects all text in the field.
setSelectionRange(start, end, direction)Sets the caret or text selection range.

Events

EventDescription
inputFires when the value changes. The event bubbles and is composed.
changeFires when a value change is committed or the field loses focus.
searchFires when Enter is pressed with the suggestions panel closed.
rich-input-selectFires when an autocomplete suggestion is selected.

rich-input-select exposes the selected suggestion through event.detail:

const input = document.querySelector('rich-input');
input.addEventListener('rich-input-select', function (event) {
  const {
    type,
    keyword,
    value,
    label,
    query
  } = event.detail;
  console.log(keyword, value, query);
});

Alternatives & Related Resources

FAQs

Q: Can rich-input load autocomplete values from an API?
A: Remote data is not a built-in data-source mode. Fetch the values in application code, populate or update the nested <datalist>, and rich-input will detect those changes.

Q: Can rich-input submit through a normal HTML form?
A: Yes. Set its name and the current value participates in standard form submission and FormData.

Q: Does rich-input always use a native <input> internally?
A: The OpaqueRange implementation uses a native text input. On browsers that need its highlighting fallback, the component can switch to an adapted single-line contenteditable element inside its Shadow DOM.

You Might Be Interested In:


Leave a Reply