Rich Search Input With Token Highlighting – rich-input

Category: Form , Javascript | September 21, 2026
Authorbramus
Last UpdateSeptember 21, 2026
LicenseMIT
Views19 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 filter names, combinators, and allowed values, highlights recognized query tokens inside the field, and uses nested <datalist> elements for filter configuration.

Queries can also use configurable prefix operators, boolean combinators, and grouping delimiters.

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

Shadow Parts expose the control for CSS customization, while getParsedQuery() returns the raw query and an ordered token list for application logic.

Features

  • Free text and structured keyword:value filters in one search field.
  • Autocomplete for filter names, combinators, and filter values, with matching based on the complete active token.
  • Declarative <datalist> configuration with runtime updates.
  • Configurable prefix operators, boolean combinators, and grouping delimiter pairs.
  • 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 styling for filter values, operators, combinators, delimiters, keywords, and invalid tokens through the CSS Custom Highlight API.
  • Shadow Parts for control and suggestion styling.
  • 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. The optional syntax attributes configure operators, combinators, and delimiter pairs.

<rich-input
  name="q"
  operators="-"
  combinators="AND OR"
  delimiters="()"
  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. The configured syntax can group filters, join them with boolean combinators, or prefix a filter with an operator:

(brand:"Northstar Audio" OR year:2026) AND -category:Speakers

Datalist & Syntax Configuration

Element / AttributeTypeDescription
<rich-input operators="...">stringSpace-separated single-character prefix operators. Defaults to "-". Set an empty value to disable operators.
<rich-input combinators="...">stringSpace-separated boolean query combinators such as AND OR NOT. The default is empty.
<rich-input delimiters="...">stringSpace-separated opening/closing delimiter pairs such as () [] {}. Defaults to "()". Set an empty value to disable delimiters.
<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 also registers highlight names for operators, combinators, delimiters, keyword prefixes, and invalid tokens.

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-operator) {
  color: #e11d48;
}
::highlight(rich-input-combinator) {
  color: #7c3aed;
}
::highlight(rich-input-delimiter) {
  color: #0284c7;
}
::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)Leading icon element. Customize the default search icon with CSS content or background.
::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-combinator)Combinator 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.

Customize the Leading Icon

The leading search icon is exposed through ::part(icon). Replace it with CSS content or a custom background image.

rich-input::part(icon) {
  content: "🎵";
}
/* Or use a custom image */
rich-input::part(icon) {
  content: "";
  background: url("music-note.svg") no-repeat center / contain;
}

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() returns the raw query string and an ordered tokens array. Tokens can represent keywords, combinators, delimiters, text, and whitespace.

const input = document.querySelector('rich-input');
input.value = '-brand:Acme OR year:2026';
console.log(input.getParsedQuery());

The result uses this structure:

{
  raw: '-brand:Acme OR year:2026',
  tokens: [
    {
      type: 'keyword',
      operator: '-',
      keyword: 'brand',
      innerValue: 'Acme',
      start: 0,
      end: 11
    },
    {
      type: 'whitespace',
      raw: ' ',
      start: 11,
      end: 12
    },
    {
      type: 'combinator',
      raw: 'OR',
      combinator: 'OR',
      start: 12,
      end: 14
    },
    {
      type: 'whitespace',
      raw: ' ',
      start: 14,
      end: 15
    },
    {
      type: 'keyword',
      operator: null,
      keyword: 'year',
      innerValue: '2026',
      start: 15,
      end: 24
    }
  ]
}

Update Datalists at Runtime

rich-input watches its datalists and syntax attributes for changes. Insert, remove, or update configuration after initialization to refresh the available query syntax 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.
operatorsstring[] | stringGets or sets prefix operators for this instance. Defaults to RichInput.operators (['-']). Set null to clear the local override.
RichInput.operatorsstring[] | stringStatic global default operators for instances with no local override.
combinatorsstring[] | stringGets or sets boolean combinators for this instance. Defaults to RichInput.combinators ([]). Set null to clear the local override.
RichInput.combinatorsstring[] | stringStatic global default combinators for instances with no local override.
delimitersstring[] | stringGets or sets delimiter pairs for this instance. Defaults to RichInput.delimiters (['()']). Set null to clear the local override.
RichInput.delimitersstring[] | stringStatic global default delimiter pairs for instances with no local override.
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, tokens } for the current query.
getKeywords()Returns the configured datalist keyword definitions and options.
getOperators() / setOperators(operators)Gets or sets operators for this instance. Static RichInput.getOperators() and RichInput.setOperators() manage the global defaults.
getCombinators() / setCombinators(combinators)Gets or sets combinators for this instance. Static RichInput.getCombinators() and RichInput.setCombinators() manage the global defaults.
getDelimiters() / setDelimiters(delimiters)Gets or sets delimiter pairs for this instance. Static RichInput.getDelimiters() and RichInput.setDelimiters() manage the global defaults.
getActiveValueRanges()Returns active value highlight range descriptors.
getActiveKeywordRanges()Returns active keyword highlight range descriptors.
getActiveOperatorRanges()Returns active operator highlight range descriptors.
getActiveCombinatorRanges()Returns active combinator highlight range descriptors.
getActiveDelimiterRanges()Returns active delimiter highlight range descriptors.
getActiveInvalidRanges()Returns active invalid-token highlight range descriptors.
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,
    operator,
    keyword,
    value,
    label,
    query
  } = event.detail;
  console.log(operator, 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.

Changelog

v1.4 (09/21/2026)

  • Support for (configurable) operators (e.g. +, -, ~, …)
  • Support for (configurable) combinators (e.g. AND, OR, NOT, …)
  • Support for (configurable) delimiters (e.g. (), {}, …)
  • Only expose the raw string and tokenized tokens in getParsedQuery()
  • Scope default shadow DOM ::highlight(…) rules to :host::highlight(…)
  • Set the default text color on .popover and changed .suggestion-item / .suggestion-title to inherit
  • Remove the slots and simply allow customization of the icon through ::part(icon)

You Might Be Interested In:


Leave a Reply