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".
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 install rich-input
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.
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.
When a user types inside <rich-input>:
- Range Positioning: The component queries
range.getBoundingClientRect()on the active valueOpaqueRange. 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 withoutOpaqueRangesupport, it seamlessly falls back to calculating the left position via a hidden mirror<div>. - Live Highlighting: For every recognized keyword token (e.g.
label:"We Play House Recordings"), the component creates a value range withinput.createValueRange(start, end)and adds it tonew Highlight(range). - 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);
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:
| 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 |
|---|---|
leading | Custom leading icon or control. Contains the default search magnifying glass SVG (::part(icon)) as fallback content. |
trailing | Custom 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.
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.
rich-input.theme-pill::part(control) {
border-radius: 9999px;
border: 2px solid #3b82f6;
padding: 0 1.25rem;
}
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.
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.
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. |