Overview

<rich-input> brings search engine-grade filter intelligence to standard web forms. Users can search with plain free-text or structured key-value filters such as label:"We Play House Recordings", year:2026, or playlist:"WPH Classics".

Live Demonstration

Click a preset search query to load it:

Features

Built as a native, lightweight Web Component utilizing cutting-edge web platform capabilities.

🎯 Dual Autocompletion

Autocompletes both filter keywords (e.g. typing a suggests artist:, typing s suggests style:) and values (e.g. typing label:"K suggests Keinemusik and Kranky).

πŸ“ OpaqueRange Positioning

Anchors the autocomplete dropdown directly to the start of the current OpaqueRange (e.g. at the opening quotation mark of a value) using range.getBoundingClientRect() (with a hidden mirror-div fallback for browsers without OpaqueRange support).

🎨 CSS Custom Highlight API

Keywords and values are directly styled inside the native <input> using standard pseudo-elements like ::highlight(label) or ::highlight(year) without overlay <div> hacks.

🧩 Declarative <datalist>

Zero JavaScript configuration required. Add standard <datalist id="..." label="..."> elements with <option> children inside <rich-input>.

πŸ“‹ Form Associated

Implements formAssociated = true and ElementInternals so it integrates with standard <form> submission, FormData, and form reset lifecycles.

πŸ’… Full ::part() Theming

Exposes shadow parts including ::part(input), ::part(control), ::part(popover), and ::part(suggestion-item) for complete styling flexibility.

Installation & Setup

Install via npm or load directly via ESM from your favorite CDN.

npm
npm install rich-input
JavaScript Module Import
import 'rich-input';
// <rich-input> is now registered and ready to use!

Datalist Configuration

Configure filters and values declaratively by nesting <datalist> elements directly inside <rich-input>:

<rich-input placeholder="Search tracks, labels, artists...">
  <!-- String keyword with options -->
  <datalist id="label" label="Record Label">
    <option value="Defected"></option>
    <option value="Keinemusik"></option>
    <option value="Kranky"></option>
    <option value="Madhouse Records"></option>
    <option value="Ninja Tune"></option>
    <option value="Warp Records"></option>
    <option value="We Play House Recordings"></option>
    <option value="XL Recordings"></option>
  </datalist>

  <!-- Number keyword -->
  <datalist id="year" label="Release Year" data-type="number">
    <option value="2026"></option>
    <option value="2025"></option>
    <option value="2024"></option>
  </datalist>
</rich-input>
Element / Attribute Description
id on <datalist> The keyword identifier used in queries (e.g. id="label" produces label:...). Case-insensitive.
label on <datalist> Human-readable display name shown in suggestion headers. Defaults to capitalized id.
data-type on <datalist> Set to number for numeric fields (e.g. year). Defaults to string.
value on <option> The value inserted when the suggestion is accepted. Values with spaces are automatically enclosed in quotes (e.g. "We Play House Recordings").
label or text on <option> Optional descriptive label (e.g. <option value="2026" label="Current Year">).
<option> children Optional custom HTML markup (e.g. logos, avatars, icons) rendered directly inside the suggestion item.

Rich Option Markup

<rich-input> supports embedding custom HTML markup inside <datalist> options. For example, for record labels or artists, you can prepend an image with the label’s logo.

Example: Prepending a Record Label Logo HTML Markup

Embed markup directly inside the <option> element:

<rich-input placeholder="Search...">
  <datalist id="label" label="Record Label">
    <option value="Defected">
      <img
        src="assets/defected.jpg"
        height="50" width="50"
        alt="Defected Logo">
      Defected
    </option>
    <option value="Keinemusik">
      <img
        src="assets/keinemusik.jpg"
        height="50" width="50"
        alt="Keinemusik Logo">
      Keinemusik
    </option>
    <option value="Kranky">
      <img
        src="assets/kranky.jpg"
        height="50" width="50"
        alt="Kranky Logo">
      Kranky
    </option>
    <option value="Madhouse Records">
      <img
        src="assets/madhouse-records.jpg"
        height="50" width="50"
        alt="Madhouse Records Logo">
      Madhouse Records
    </option>
    <option value="Ninja Tune">
      <img
        src="assets/ninja-tune.jpg"
        height="50" width="50"
        alt="Ninja Tune Logo">
      Ninja Tune
    </option>
    <option value="Warp Records">
      <img
        src="assets/warp-records.png"
        height="50" width="50"
        alt="Warp Records Logo">
      Warp Records
    </option>
    <option value="We Play House Recordings">
      <img
        src="assets/we-play-house-recordings.jpg"
        height="50" width="50"
        alt="We Play House Recordings Logo">
      We Play House Recordings
    </option>
    <option value="XL Recordings">
      <img
        src="assets/xl-recordings.jpg"
        height="50" width="50"
        alt="XL Recordings Logo">
      XL Recordings
    </option>
  </datalist>
</rich-input>

When suggestions appear, the custom HTML markup is rendered directly inside the suggestion list. When selected, the clean value ("We Play House Recordings") is inserted into the input:

The OpaqueRange API

Traditionally, calculating caret position or styling text inside a native <input> required brittle mirror-div hacks. With Chromium 152+, the new OpaqueRange API unlocks first-class text measurement and highlighting.

How <rich-input> uses OpaqueRange

When a user types inside <rich-input>:

  1. Range Positioning: The component queries range.getBoundingClientRect() on the active value OpaqueRange. The suggestion popover is anchored at the start of the range (such as the opening quotation mark) rather than shifting with the cursor. In browsers without OpaqueRange support, it seamlessly falls back to calculating the left position via a hidden mirror <div>.
  2. Live Highlighting: For every recognized keyword token (e.g. label:"We Play House Recordings"), the component creates a value range with input.createValueRange(start, end) and adds it to new Highlight(range).
  3. Resource Cleanup: When text mutations occur, previously created ranges are cleanly disconnected using range.disconnect().
// 1. Measuring the start of the active range for the dropdown popover
const rect = valueRange.getBoundingClientRect();
popover.style.left = `${rect.left}px`;
popover.style.top = `${rect.bottom + 6}px`;

// 2. Highlighting keyword values with the Custom Highlight API
const valueRange = input.createValueRange(valStart, valEnd);
const labelHighlight = new Highlight(valueRange);
CSS.highlights.set('label', labelHighlight);
Cross-browser fallback: In browsers with no OpaqueRange support but with CSS Custom Highlight API support (such as Safari 17.2+ and Firefox 141+), <rich-input> automatically falls back to an adapted [contenteditable] element inside its shadow DOM to provide in-input ::highlight() syntax highlighting, while using a hidden mirror <div> to position the suggestions popover.

Custom Highlights Styling

Values for each configured keyword can be styled using standard CSS ::highlight(keyword) pseudo-elements in your stylesheet:

/* Style the value set in label:"Warp Records" */
::highlight(label) {
  background-color: oklch(0.92 0.08 240);
  color: oklch(0.28 0.14 240);
  text-decoration: 2px underline solid oklch(0.5 0.15 240 / 0.5);
}

/* Style numeric year values like year:2026 */
::highlight(year) {
  background-color: oklch(0.93 0.1 85);
  color: oklch(0.35 0.14 85);
}

/* Style artist names */
::highlight(artist) {
  background-color: oklch(0.92 0.1 320);
  color: oklch(0.32 0.14 320);
}

/* Generic prefix highlight for keyword labels (e.g. "label:", "year:") */
::highlight(rich-input-keyword) {
  color: #64748b;
  text-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
}

/* Invalid highlight (squiggly underline for unrecognized keywords or values not present in datalist) */
::highlight(rich-input-invalid) {
  text-decoration: underline wavy #ef4444;
  text-decoration-skip-ink: none;
}

Cross-engine compatibility: In browsers where the component falls back to a [contenteditable] element inside its Shadow DOM (such as Safari and Firefox), <rich-input> automatically syncs document-level ::highlight() rules into its shadow stylesheet to ensure highlights apply across shadow boundaries without extra markup.

Alternatively, for self-contained components or instance-specific styles, <rich-input> also accepts an optional embedded <style> block as a direct child, which is automatically injected into the shadow root.

Shadow Parts (::part) & Anatomy

All internal elements of <rich-input> are exposed for external styling through ::part() and CSS Custom Highlight pseudo-elements:

label: hou Record Label Madhouse Records We Play House Recordings <rich-input> ::part(control) ::part(icon) ::part(input) ::highlight(label) ::part(suggestions-header) ::part(suggestion-image) ::part(clear-button) ::part(popover) ::part(suggestions-list) ::part(suggestion-item) ::part(suggestion-item-active) ::part(suggestion-value)
Part Name Element Targeted
::part(input)The internal native <input type="text">
::part(control)The flex container wrapping the search icon, input, and clear button
::part(icon)The default leading search SVG icon (fallback in slot="leading")
::part(clear-button)The clear search button (visible when input has text)
::part(popover)The autocomplete suggestions popover container
::part(suggestions-header)The header bar at the top of the popover
::part(suggestions-list)The <ul> container holding suggestion items
::part(suggestion-item)Each individual suggestion <li> item
::part(suggestion-item-active)The currently focused / hovered suggestion item
::part(suggestion-content)The content container inside each suggestion item
::part(suggestion-image)Images and icons rendered inside rich suggestion items

Slots

<rich-input> exposes named slots for custom leading and trailing elements:

Slot Name Description
leadingCustom leading icon or control. Contains the default search magnifying glass SVG (::part(icon)) as fallback content.
trailingCustom content rendered after the clear button.
(default)Unnamed slot where <datalist> configuration elements are placed (visually hidden).

1. Parsed Query Inspector

Inspect real-time tokenization and the structured query data returned by getParsedQuery() as you search.

Parsed Query Inspector getParsedQuery()

Detected Tokens:

Structured Query Output (getParsedQuery()):


              

2. Form Integration

<rich-input> seamlessly participates in form submissions, validation, and FormData extraction via formAssociated = true.

3. Shadow Parts Theming (::part)

Style components completely from page CSS without breaking encapsulation.

Pill / Rounded Spotlight Theme
rich-input.theme-pill::part(control) {
  border-radius: 9999px;
  border: 2px solid #3b82f6;
  padding: 0 1.25rem;
}
Retro Terminal Theme
rich-input.theme-terminal::part(control) {
  background: #0f172a;
  border: 1px solid #22c55e;
}
rich-input.theme-terminal::part(input) {
  color: #4ade80;
  font-family: monospace;
}

4. Rich Option & Image Styling (::part)

When options contain images (such as logos, icons, or avatars), the component exposes them via ::part(suggestion-image) for external CSS styling.

Circular Logo Avatar via ::part(suggestion-image) CSS Shadow Parts

In this example, the record label logos are styled via ::part(suggestion-image) as circular avatars:

/* Style suggestion logos as circular avatar icons */
rich-input::part(suggestion-image) {
  width: 2.5em;
  height: 2.5em;
  border-radius: 50%;
  object-fit: cover;
  outline: 1px solid #ccc;
}

5. Custom Leading Icon & Slots

Pass a custom icon via slot="leading". The default search magnifying glass SVG is provided as fallback content inside the slot, so supplying a slotted element automatically replaces it without requiring any CSS overrides.

Music Note Icon via slot="leading" HTML Slots

In this example, a custom music note SVG replaces the default search icon, and genre values are highlighted via ::highlight(genre):

<rich-input value='genre:"Deep House"' placeholder="Search musical genres (e.g. type 'genre:')...">
  <!-- Custom leading icon replaces the default search magnifying glass -->
  <svg slot="leading" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
    <path d="M9 18V5l12-2v13"></path>
    <circle cx="6" cy="18" r="3"></circle>
    <circle cx="18" cy="16" r="3"></circle>
  </svg>

  <datalist id="genre" label="Music Genre">
    <option value="Acid House"></option>
    <option value="Ambient"></option>
    <option value="Chicago House"></option>
    <option value="Deep House"></option>
    <option value="Dub Techno"></option>
    <option value="Electro"></option>
  </datalist>
</rich-input>

6. Dynamic Datalists

Add or update <datalist> elements dynamically at runtime. The component uses a MutationObserver and slotchange event to immediately update available keywords and suggestions.

Clicking the button above injects <datalist id="bpm" label="Beats Per Minute"> into all <rich-input> instances on the page.

JavaScript API

Methods, properties, and events exposed by the RichInput class.

Member Type Description
value string Gets or sets the current search text, refreshing highlights and form values.
getParsedQuery() () => Object Returns structured breakdown with { raw, text, keywords, tokens }.
getKeywords() () => Array Returns array of all configured datalist keywords and options.
focus(), blur(), select() Function Standard focus and text selection manipulation methods.
setSelectionRange() Function Sets caret or text selection range on the internal input.
rich-input-select CustomEvent Dispatched when a suggestion is selected (detail includes type, keyword, value, query).
search CustomEvent Dispatched when user presses Enter with suggestions closed.