Zum Inhalt springen
← Zurück zu den Projekten

Rich Input

#rich-input

A rich input field <rich-input> with keyword-based autocomplete and in-input highlighting powered by <datalist>, the OpaqueRange API, and the CSS Custom Highlight API.

License: MIT Custom Elements

The <rich-input> component is a rich input field that acts like a standard <input type="text"> so users can type ordinary text or search terms, but enhances it with contextual autocomplete and in-input highlighting for structured keyword:value entries (such as label:"We Play House Recordings" year:2026 playlist:"WPH Classics").


#Features

  • Standard Input Ergonomics: Acts and feels like a regular <input type="text"> with standard value access, selection ranges, and events.
  • Dual Contextual Autocomplete:
    • Keywords: Typing at the start of a token (e.g. typing a) suggests configured keywords like artist: and style:.
    • Values: Typing within a keyword value (e.g. label:"K or label:K) suggests matching options like "Keinemusik" and "Kranky".
  • Range-Based Positioning via OpaqueRange: Positions autocomplete dropdown popovers anchored to the start of the active OpaqueRange (e.g. at the opening quotation mark of a value) using range.getBoundingClientRect(), rather than shifting with the cursor (with a hidden mirror-div fallback in unsupported browsers).
  • Native In-Input Highlighting via CSS Custom Highlight API: Highlights keyword values inside the <input> control using standard CSS rules like ::highlight(label) or ::highlight(year) without brittle mirror-div overlays.
  • Declarative Configuration via <datalist>: Configure keywords and options purely in HTML by nesting standard <datalist> elements with <option> tags inside <rich-input>.
  • Rich Option Markup: Embed custom HTML markup (such as logos, images, icons, and avatars) directly inside <option> elements for rich, visual suggestion popovers.
  • Form Associated: Implements static formAssociated = true and ElementInternals to participate seamlessly in <form> submission, FormData, and form reset lifecycles.
  • Shadow Parts Theming (::part): Full CSS customizability using ::part(input), ::part(control), ::part(popover), ::part(suggestion-item), etc.
  • Accessible (W3C Combobox Pattern): ARIA 1.2 compliant combobox with keyboard navigation (ArrowUp, ArrowDown, Enter, Tab, Escape), aria-expanded, and aria-activedescendant.
  • Graceful Fallback: Automatically feature-detects browser capabilities. In browsers with no OpaqueRange support but with CSS Custom Highlight API support (e.g. Safari 17.2+, Firefox 141+), it falls back to an adapted [contenteditable] element to provide in-input syntax highlighting via standard DOM Ranges. In environments without OpaqueRange, dropdown popovers are positioned using a hidden mirror-div measurement fallback.

#Component Anatomy & Shadow Parts

The visual below illustrates the internal Shadow DOM elements, exposed CSS Shadow Parts (::part), and CSS Custom Highlight pseudo-elements (::highlight), showing how they relate to one another:

<rich-input> Component Anatomy, Shadow Parts, and Highlight Pseudos

  • <rich-input>: The host custom element wrapping the control, datalists, and suggestions popover.
  • ::part(control): The outer input container enclosing the icon, input, and clear button.
  • ::part(icon): The default leading search magnifying glass SVG icon (fallback in slot="leading").
  • ::highlight(<keyword>): Target pseudo-element for styling keyword values via the CSS Custom Highlight API (e.g. ::highlight(label), ::highlight(year)).
  • ::highlight(rich-input-keyword): Target pseudo-element for styling keyword prefixes (e.g. label:, year:).
  • ::highlight(rich-input-invalid): Target pseudo-element for marking unrecognized keywords or invalid keyword values (not in datalist) with a squiggly underline.
  • ::part(clear-button): The clear button (visible when text is present).
  • ::part(popover): The autocomplete dropdown popover container anchored to the start of the active range via OpaqueRange (or mirror-div fallback).
  • ::part(suggestions-header): The header bar at the top of the suggestions popover.
  • ::part(suggestions-list): The <ul> container holding autocomplete suggestion items.
  • ::part(suggestion-item): Each suggestion <li> row.
  • ::part(suggestion-item-active): The currently focused / hovered suggestion row.
  • ::part(suggestion-item-selected): The suggestion row matching the value currently echoed in the input.
  • ::part(suggestion-image): The circular logo, icon, or avatar image prepended to rich suggestions.
  • ::part(suggestion-keyword): The keyword label text inside a keyword suggestion.
  • ::part(suggestion-value): The value label text inside a value suggestion.

#Quick Start

#1. Installation

Install via npm:

npm install rich-input

Or import directly in your HTML/JavaScript bundle:

import 'rich-input';

Or via CDN:

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

#2. Basic Usage

Nest <datalist> elements inside <rich-input> to configure keywords and autocomplete suggestions:

<rich-input placeholder="Search music catalog...">
  <datalist id="label" label="Record Label">
    <option value="Defected"></option>
    <option value="House"></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>

  <datalist id="year" label="Release Year" data-type="number">
    <option value="2026"></option>
    <option value="2025"></option>
    <option value="2024"></option>
  </datalist>

  <datalist id="playlist" label="Playlist">
    <option value="WPH Classics"></option>
    <option value="Late Night Grooves"></option>
  </datalist>
</rich-input>

#Datalist Configuration

Configuration is defined by standard HTML <datalist> elements placed inside the <rich-input> element:

Element / Attribute Type Description
<datalist id="..."> string Required. The keyword identifier used in queries (e.g. id="artist" produces artist:). Case-insensitive.
<datalist label="..."> string Human-readable label displayed in suggestion headers. Defaults to capitalized id.
<datalist data-type="..."> string Optional data type ("string" or "number").
<option value="..."> string The suggested value. If the value contains spaces, quotes are automatically added when inserted (e.g. "We Play House Recordings").
<option label="..."> string Optional descriptive label shown alongside the value.
<option> children Node Optional image (<img>) prepended to the suggested value.

Datalists can be added, updated, or removed dynamically at runtime; <rich-input> observes changes via slotchange and MutationObserver.


#Rich Option Markup

<rich-input> supports rich HTML markup inside <option> elements. For example, for record labels or artists, you can prepend a logo image:

<rich-input placeholder="Search...">
  <datalist id="label" label="Record Label">
    <option value="Defected">
      <img src="https://raw.githubusercontent.com/bramus/rich-input/main/assets/defected.jpg" height="50" width="50" alt="Defected Logo">
      Defected
    </option>
    <option value="House">
      <img src="https://raw.githubusercontent.com/bramus/rich-input/main/assets/house.jpg" height="50" width="50" alt="House Logo">
      House
    </option>
    <option value="Keinemusik">
      <img src="https://raw.githubusercontent.com/bramus/rich-input/main/assets/keinemusik.jpg" height="50" width="50" alt="Keinemusik Logo">
      Keinemusik
    </option>
    <option value="Kranky">
      <img src="https://raw.githubusercontent.com/bramus/rich-input/main/assets/kranky.jpg" height="50" width="50" alt="Kranky Logo">
      Kranky
    </option>
    <option value="Madhouse Records">
      <img src="https://raw.githubusercontent.com/bramus/rich-input/main/assets/madhouse-records.jpg" height="50" width="50" alt="Madhouse Records Logo">
      Madhouse Records
    </option>
    <option value="Ninja Tune">
      <img src="https://raw.githubusercontent.com/bramus/rich-input/main/assets/ninja-tune.jpg" height="50" width="50" alt="Ninja Tune Logo">
      Ninja Tune
    </option>
    <option value="Warp Records">
      <img src="https://raw.githubusercontent.com/bramus/rich-input/main/assets/warp-records.png" height="50" width="50" alt="Warp Records Logo">
      Warp Records
    </option>
    <option value="We Play House Recordings">
      <img src="https://raw.githubusercontent.com/bramus/rich-input/main/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="https://raw.githubusercontent.com/bramus/rich-input/main/assets/xl-recordings.jpg" height="50" width="50" alt="XL Recordings Logo">
      XL Recordings
    </option>
  </datalist>
</rich-input>

When suggesting values for label:, <rich-input> sniffs the image inside the <option>, renders it alongside the option's text content, and exposes ::part(suggestion-image) for external CSS styling (e.g. as a 1em circular icon):

rich-input::part(suggestion-image) {
  width: 1em;
  height: 1em;
  border-radius: 50%;
  object-fit: cover;
}

#Custom Leading Icon & Slots

<rich-input> provides named slots to customize elements inside the control:

  • slot="leading": Replace the leading icon. The default magnifying glass SVG is provided as fallback content inside the slot, so passing a custom element into slot="leading" automatically replaces it without needing CSS overrides.
  • slot="trailing": Add controls or elements after the clear button (e.g. submit button, voice input, keyboard shortcut badge).

#Passing a Custom Leading Icon

Provide your own SVG or image with slot="leading":

<rich-input placeholder="Search music catalog...">
  <!-- Custom leading music icon -->
  <svg slot="leading" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
    <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="Genre">
    <option value="House"></option>
    <option value="Techno"></option>
  </datalist>
</rich-input>

When an element with slot="leading" is supplied, the default search magnifying glass icon is automatically suppressed. If no slotted element is provided, the default magnifying glass icon renders as fallback.

#Available Slots

Slot Name Description
leading Custom leading icon or content. Defaults to the search magnifying glass icon (::part(icon)).
trailing Custom content rendered after the clear button.
(default) Unnamed slot where <datalist> configuration elements are placed (visually hidden).

#The OpaqueRange API

The OpaqueRange API is a web platform standard introduced in Chromium 152 (Google Chrome, Microsoft Edge) that enables range-based operations over the text content of form controls (<input> and <textarea>).

Before OpaqueRange, web authors had to clone form controls into hidden <div>s to measure caret coordinates or apply highlights. OpaqueRange provides native access:

// Measure exact caret coordinates inside <input>
const range = input.createValueRange(caretPos, caretPos);
const rect = range.getBoundingClientRect();

// Anchor autocomplete popover at caret
popover.style.left = `${rect.left}px`;
popover.style.top = `${rect.bottom + 4}px`;

// Highlight syntax directly inside <input>
const valueRange = input.createValueRange(valStart, valEnd);
const highlight = new Highlight(valueRange);
CSS.highlights.set('label', highlight);

<rich-input> automatically checks typeof HTMLInputElement.prototype.createValueRange === 'function'. On supported browsers (Chromium 152+), caret tracking and ::highlight() are applied natively via OpaqueRange.

In browsers without OpaqueRange that support the CSS Custom Highlight API (such as Safari 17.2+ and Firefox 141+), <rich-input> automatically falls back to an adapted single-line [contenteditable] element inside its shadow DOM. This exposes standard DOM Text nodes so standard Range objects can be created and passed to CSS.highlights, delivering on-the-fly ::highlight() syntax highlighting across browsers. In environments lacking range positioning, popover dropdowns are positioned using a hidden mirror <div> text measurement fallback.


#Styling Highlights with the CSS Custom Highlight API

Values corresponding to configured keywords are registered into the global CSS.highlights registry and 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;
}

In browsers using the [contenteditable] fallback inside Shadow DOM (such as Safari and Firefox), <rich-input> automatically syncs document-level ::highlight() rules into its shadow stylesheet so that highlighting works across shadow boundaries without extra markup.

For self-contained widgets or instance-specific style overrides, <rich-input> also supports an optional embedded <style> block as a direct child, which is automatically injected into the shadow root:

<rich-input value='artist:"Aphex Twin" label:"Warp Records"'>
  <style>
    ::highlight(label) {
      background-color: #dbeafe;
      color: #1e40af;
    }
  </style>
  <datalist id="label" label="Record Label">...</datalist>
</rich-input>

Note: Supported CSS properties on ::highlight() include color, background-color, text-decoration, text-shadow, -webkit-text-stroke-color, -webkit-text-stroke-width, and -webkit-text-fill-color.


#Styling the Input with Shadow Parts (::part)

Every internal element of <rich-input> is exposed via ::part():

/* Style the outer control container */
rich-input::part(control) {
  border-radius: 9999px;
  border: 2px solid #2563eb;
  padding: 0 1.25rem;
  background: #ffffff;
}

/* Style the native text input */
rich-input::part(input) {
  font-family: 'JetBrains Mono', monospace;
  font-size: 1rem;
}

/* Style the suggestions popover */
rich-input::part(popover) {
  border-radius: 12px;
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15);
}

/* Style active suggestion item */
rich-input::part(suggestion-item-active) {
  background-color: #dbeafe;
}

#Available Shadow Parts

Part Name Description
::part(control) The wrapper container enclosing the search icon, input, and clear button
::part(input) The internal native <input type="text">
::part(icon) The default leading search icon SVG (fallback in slot="leading")
::part(clear-button) The clear button (visible when text is present)
::part(popover) The autocomplete popover container
::part(suggestions-header) The header bar at the top of the popover
::part(suggestions-list) The <ul> list element
::part(suggestion-item) Each suggestion <li> item
::part(suggestion-item-active) The currently focused / hovered suggestion item
::part(suggestion-item-selected) The suggestion item matching the value currently echoed in the input
::part(suggestion-keyword) Keyword name element in suggestion items
::part(suggestion-value) Value element in suggestion items
::part(suggestion-content) The content container inside each suggestion item
::part(suggestion-image) Image or icon element rendered inside rich suggestion items

#JavaScript API

#Properties

  • value (string): Gets or sets the search input value. Updates highlights and form value automatically.
  • placeholder (string): Gets or sets the input placeholder text.
  • disabled (boolean): Disables or enables the input control.
  • name (string): Form field name when submitted inside a <form>.
  • selectionStart / selectionEnd (number): Text selection / cursor indices.

#Methods

  • getParsedQuery(): Returns a parsed object representing the search query:
    {
      "raw": "label:\"We Play House Recordings\" year:2026 chicago house",
      "text": "chicago house",
      "keywords": {
        "label": ["We Play House Recordings"],
        "year": ["2026"]
      },
      "tokens": [...]
    }
    
  • getKeywords(): Returns an array of configured keyword definitions from the datalists.
  • focus(options): Focuses the internal input.
  • blur(): Removes focus from the internal input.
  • select(): Selects all text inside the input.
  • setSelectionRange(start, end, direction): Sets caret or selection range.

#Events

  • input: Dispatched when the search value changes (bubbles, composed).
  • change: Dispatched on blur or when a search change is committed.
  • search: Dispatched when the user presses Enter with suggestions closed.
  • rich-input-select: Dispatched when an autocomplete suggestion is selected.
    • event.detail: { type, keyword, value, label, query }

#Form Integration

<rich-input> supports native <form> submission through standard ElementInternals:

<form id="search-form" action="/search" method="GET">
  <rich-input name="q" placeholder="Search tracks...">
    <datalist id="genre" label="Genre">
      <option value="House"></option>
      <option value="Techno"></option>
    </datalist>
  </rich-input>
  <button type="submit">Search</button>
</form>

<script>
  const form = document.getElementById('search-form');
  form.addEventListener('submit', (e) => {
    e.preventDefault();
    const data = new FormData(form);
    console.log('Submitted query:', data.get('q'));
  });
</script>

#Building from Source

# Build ./dist package
npm run build

# Start local demo server
npm start

#License

MIT © Bramus Van Damme

Neue Version verfügbar.