npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

elastic-input

v0.11.0

Published

Syntax-aware smart autocomplete input for Elastic query syntax

Readme

elastic-input

A syntax-aware smart autocomplete input for building structured queries. Supports field:value pairs, boolean operators, comparison operators, saved searches, history references, date pickers, and real-time validation — all in a single React component.

Built with React functional components and hooks (compatible with React 16.8+), zero runtime dependencies beyond React/ReactDOM, and fully inline-styled (no CSS imports required).

Live Demo

Features

  • Syntax highlighting — field names, values, operators, quoted strings, and special tokens are color-coded in real time
  • Context-aware autocomplete — suggestions adapt based on cursor position (field names, values, operators, saved searches, history)
  • Built-in date picker — calendar UI with single date and date range selection for date-typed fields
  • Validation with squiggly underlines — unknown fields, type mismatches, and custom validators shown as red wavy underlines with hover tooltips
  • Deferred error display — validation errors only appear after the cursor leaves the error range
  • Saved searches (#) — reference saved queries by name with autocomplete
  • History references (!) — recall previous searches with autocomplete
  • Keyboard-driven — Tab to accept + continue, Enter to accept + submit, Ctrl+Enter to force submit, arrow keys to navigate
  • Fully configurable — colors, structural styles, fonts, and layout are all customizable via props
  • Dark mode ready — ships with DARK_COLORS and DARK_STYLES presets

Installation

npm install elastic-input

Quick Start

import { ElasticInput } from 'elastic-input';
import type { FieldConfig } from 'elastic-input';

const fields: FieldConfig[] = [
  {
    name: 'status',
    label: 'Status',
    type: 'string',
    description: 'Account status',
    placeholder: 'Search statuses...',
  },
  {
    name: 'created',
    label: 'Created Date',
    type: 'date',
    description: 'When the record was created',
  },
  {
    name: 'price',
    label: 'Price',
    type: 'number',
    description: 'Item price',
  },
  {
    name: 'is_active',
    label: 'Active',
    type: 'boolean',
  },
];

function App() {
  return (
    <ElasticInput
      fields={fields}
      placeholder="Search... e.g. status:active AND price:>100"
      onSearch={(query, ast) => {
        console.log('Search:', query);
        console.log('AST:', ast);
      }}
      onChange={(query, ast) => {
        console.log('Changed:', query);
      }}
    />
  );
}

Query Syntax

| Syntax | Example | Description | |--------|---------|-------------| | field:value | status:active | Field equals value | | field:"quoted value" | name:"John Doe" | Quoted value with spaces | | field:>value | price:>100 | Greater than (also >=, <, <=) | | AND / OR / NOT | a AND b OR NOT c | Boolean operators (case-insensitive) | | (...) | (a OR b) AND c | Grouping with parentheses | | -field:value | -status:inactive | Negation (shorthand for NOT) | | #name | #vip-active | Saved search reference | | !partial | !recent | History search reference | | value* | stat* | Wildcard matching | | "phrase" | "error occurred" | Bare phrase (full-text) |

Implicit AND is supported — status:active level:ERROR is equivalent to status:active AND level:ERROR.

Props

Required

| Prop | Type | Description | |------|------|-------------| | fields | FieldConfig[] \| () => Promise<FieldConfig[]> | Field definitions for autocomplete and validation |

Optional

| Prop | Type | Default | Description | |------|------|---------|-------------| | onSearch | (query, ast) => void | — | Called on search submission | | onChange | (query, ast) => void | — | Called on every input change | | onValidationChange | (errors) => void | — | Called when validation errors change | | value | string | — | Controlled input value | | defaultValue | string | — | Initial uncontrolled value | | savedSearches | SavedSearch[] \| (partial) => Promise<SavedSearch[]> | — | Saved search definitions (sync array or async callback with partial) | | searchHistory | HistoryEntry[] \| (partial) => Promise<HistoryEntry[]> | — | Search history entries (sync array or async callback with partial) | | fetchSuggestions | (field, partial) => Promise<SuggestionItem[]> | — | Async suggestion provider for field values (called for all non-boolean fields) | | colors | ColorConfig | DEFAULT_COLORS | Syntax highlighting and UI colors | | styles | StyleConfig | DEFAULT_STYLES | Structural/layout style overrides | | placeholder | string | "Search..." | Placeholder text | | className | string | — | CSS class for the outer container | | classNames | ClassNamesConfig | — | Custom CSS classes for sub-elements (editor, dropdown, tokens, etc.) | | style | CSSProperties | — | Inline styles for the outer container | | inputRef | (api) => void | — | Receive an imperative API handle | | dropdown | DropdownConfig | {} | Dropdown behavior and rendering (open, triggers, renderers) | | features | FeaturesConfig | {} | Feature toggles (multiline, smartSelectAll, expandSelection, wildcardWrap, savedSearches, historySearch) | | onKeyDown | (e) => void | — | Called before internal keyboard handling | | onFocus | () => void | — | Called when the input gains focus | | onBlur | () => void | — | Called when the input loses focus | | onTab | (context) => TabActionResult | — | Override Tab key behavior (accept/blur/submit) | | validateValue | (ctx) => ValidateReturn | — | Custom validation for all value types | | parseDate | (value: string) => Date \| null | — | Custom date parser for validation and date picker init | | plainModeLength | number | — | Character count at which highlighting, autocomplete, and validation are disabled for performance | | interceptPaste | (text, event) => string \| null \| Promise<…> | — | Transform or cancel pasted text before insertion; supports async |

Field Configuration

interface FieldConfig {
  name: string;           // Field identifier used in queries
  label?: string;         // Display label (used in autocomplete)
  type: FieldType;        // 'string' | 'number' | 'date' | 'boolean' | 'ip'
  aliases?: string[];     // Alternative names that resolve to this field
  operators?: string[];   // Allowed operators (future use)
  description?: string;   // Shown in autocomplete dropdown
  placeholder?: string | false; // Hint shown while typing a value (false to suppress)
  suggestions?: boolean;        // Whether fetchSuggestions is called (default true)
  hide?: boolean;               // Exclude from autocomplete dropdown but still validate without "Unknown field"
}

Field Types

| Type | Autocomplete | Validation | Comparison Ops | |------|-------------|------------|----------------| | boolean | Shows true / false | Must be true or false | No | | number | Shows hint "Enter a number" | Must be numeric | Yes (>, >=, <, <=) | | date | Opens date picker with calendar | ISO dates, relative dates (now-7d) | Yes | | ip | Shows hint "Enter an IP address" | Valid IPv4, supports wildcards (192.168.*) | No | | string | No default hint (use placeholder for custom) | No validation (anything accepted) | No |

Imperative API

Access via inputRef:

let api;

<ElasticInput
  fields={fields}
  inputRef={(ref) => { api = ref; }}
/>

// Later:
api.getValue();              // Returns current query string
api.setValue('status:active');// Sets query programmatically
api.focus();                 // Focuses the input
api.blur();                  // Blurs the input
api.getAST();                // Returns the parsed AST
api.getValidationErrors();   // Returns current validation errors

Validation

Validation runs automatically on every input change. Errors appear as red wavy underlines beneath the invalid text. Hover over a squiggly to see the error message.

Deferred Display

Errors are only shown visually after the cursor moves away from the error range, so the user isn't distracted while still typing.

External Error Access

Use onValidationChange to receive errors outside the component:

<ElasticInput
  fields={fields}
  onValidationChange={(errors) => {
    // errors: Array<{ message: string, start: number, end: number, field?: string }>
    if (errors.length > 0) {
      console.log('Validation errors:', errors);
    }
  }}
/>

Or use the imperative API:

const errors = api.getValidationErrors();

Custom Validators

const fields: FieldConfig[] = [
  {
    name: 'rating',
    type: 'number',
    validate: (value) => {
      const n = Number(value);
      return (n >= 1 && n <= 5) ? null : 'Rating must be between 1 and 5';
    },
  },
  {
    name: 'phone',
    type: 'string',
    validate: (value) =>
      /^[\d\-\+\(\)\s]+$/.test(value) ? null : 'Invalid phone format',
  },
];

Saved Searches

Reference saved queries with #:

const savedSearches = [
  { id: '1', name: 'vip-active', query: 'status:active AND is_vip:true', description: 'All active VIPs' },
  { id: '2', name: 'high-value', query: 'deal_value:>10000', description: 'Deals over $10k' },
];

<ElasticInput
  fields={fields}
  savedSearches={savedSearches}
/>

Type # in the input to see saved search suggestions. Selecting one replaces the #token with the saved query text.

Supports async loading with per-keystroke filtering:

<ElasticInput
  fields={fields}
  savedSearches={async (partial) => {
    const res = await fetch(`/api/saved-searches?q=${partial}`);
    return res.json();
  }}
/>

Search History

Reference previous searches with !:

const history = [
  { query: 'status:active AND deal_value:>5000', label: 'Active high-value deals' },
  { query: 'level:ERROR AND service:api-gateway', label: 'API errors' },
];

<ElasticInput
  fields={fields}
  searchHistory={history}
/>

Type ! to see history suggestions. Selecting one inserts the query (wrapped in parentheses if it contains boolean operators).

Supports async loading with per-keystroke filtering:

<ElasticInput
  fields={fields}
  searchHistory={async (partial) => {
    const res = await fetch(`/api/history?q=${partial}`);
    return res.json();
  }}
/>

Async Suggestions

Provide dynamic suggestions for field values. Called for all non-boolean field value contexts when provided:

<ElasticInput
  fields={fields}
  fetchSuggestions={async (fieldName, partial) => {
    const res = await fetch(`/api/suggest?field=${fieldName}&q=${partial}`);
    const data = await res.json();
    return data.map(item => ({
      text: item.value,
      label: item.display,
      description: item.desc,
      type: fieldName,
    }));
  }}
  dropdown={{ suggestDebounceMs: 300 }}
/>

Keyboard Shortcuts

| Key | Context | Behavior | |-----|---------|----------| | Tab | Dropdown open | Accept suggestion; append space if completing a value/search/history at end of input | | Enter | Dropdown open (field value) | Accept value and submit search | | Enter | Dropdown open (other) | Accept suggestion without submitting | | Enter | No dropdown | Submit search | | Ctrl+Enter | Any | Force submit, bypassing autocomplete | | Escape | Dropdown/picker open | Close without accepting | | Arrow Up/Down | Dropdown open | Navigate suggestions | | Arrow Left/Right | Any | Move cursor; suggestions update for new position |

Theming

Colors

import { ElasticInput, DARK_COLORS } from 'elastic-input';
import type { ColorConfig } from 'elastic-input';

// Use the built-in dark preset
<ElasticInput fields={fields} colors={DARK_COLORS} />

// Or customize individual colors
const myColors: ColorConfig = {
  fieldName: '#0550ae',
  fieldValue: '#1a7f37',
  operator: '#cf222e',
  booleanOp: '#8250df',
  quoted: '#0a3069',
  paren: '#656d76',
  savedSearch: '#bf8700',
  historyRef: '#6639ba',
  wildcard: '#953800',
  error: '#cf222e',
  background: '#ffffff',
  text: '#1f2328',
  placeholder: '#656d76',
  cursor: '#1f2328',
  dropdownSelected: '#0969da',
  dropdownHover: '#f6f8fa',
  // Per-field-type value colors (overrides fieldValue for typed fields)
  valueTypes: {
    string: '#0550ae',
    number: '#0a3069',
    date: '#8250df',
    boolean: '#cf222e',
    ip: '#116329',
  },
};

Structural Styles

import { ElasticInput, DARK_STYLES } from 'elastic-input';
import type { StyleConfig } from 'elastic-input';

const myStyles: StyleConfig = {
  fontFamily: "'JetBrains Mono', monospace",
  fontSize: '16px',
  inputPadding: '12px 16px',
  inputBorderRadius: '12px',
  inputFocusBorderColor: '#7c3aed',
  inputFocusShadow: '0 0 0 3px rgba(124, 58, 237, 0.3)',
  dropdownBorderRadius: '12px',
  dropdownShadow: '0 12px 32px rgba(0, 0, 0, 0.2)',
};

<ElasticInput fields={fields} styles={myStyles} />

Full Dark Mode

import { DARK_COLORS, DARK_STYLES } from 'elastic-input';

<ElasticInput
  fields={fields}
  colors={DARK_COLORS}
  styles={DARK_STYLES}
/>

AST Output

The onSearch and onChange callbacks receive a parsed AST alongside the raw query string. AST node types:

| Node Type | Description | Example | |-----------|-------------|---------| | FieldValue | Field:value pair | status:active | | BooleanExpr | AND/OR expression | a AND b | | Not | Negation | NOT x, -x | | Group | Parenthesized group | (a OR b) | | BareTerm | Unstructured text | hello, "phrase" | | SavedSearch | Saved search ref | #my-search | | HistoryRef | History ref | !recent | | Error | Parse error | malformed input |

All nodes include start and end character offsets for mapping back to the source text.

Advanced: Using the Parser Directly

The lexer, parser, and validator are exported for standalone use:

import { Lexer, Parser, Validator } from 'elastic-input';
import type { FieldConfig } from 'elastic-input';

const query = 'status:active AND price:>100';

// Tokenize
const lexer = new Lexer(query);
const tokens = lexer.tokenize();

// Parse to AST
const parser = new Parser(tokens);
const ast = parser.parse();

// Validate
const fields: FieldConfig[] = [
  { name: 'status', type: 'string' },
  { name: 'price', type: 'number' },
];
const validator = new Validator(fields);
const errors = validator.validate(ast);

Standalone Syntax Highlighting

The syntax highlighter is a pure function — no React or DOM required. Use it to render highlighted queries anywhere (read-only displays, logs, documentation):

import { Lexer, buildHighlightedHTML, DEFAULT_COLORS } from 'elastic-input';

const tokens = new Lexer('status:active AND price:>100').tokenize();
const html = buildHighlightedHTML(tokens, DEFAULT_COLORS);
// Returns an HTML string with inline styles — set innerHTML on any element

Pass HighlightOptions for matched-paren highlighting:

buildHighlightedHTML(tokens, DEFAULT_COLORS, { cursorOffset: 5 });

Query Formatting

Pretty-print messy or minified queries with formatQuery — a pure function (no React or DOM required):

import { formatQuery } from 'elastic-input';

formatQuery('(status:active OR status:lead) AND deal_value:>5000 AND NOT tags:churned');
// (status:active OR status:lead)
// AND deal_value:>5000
// AND NOT tags:churned

formatQuery('(  (status:active AND deal_value:>10000) OR (status:lead AND tags:enterprise)  ) AND created:[2024-01-01 TO 2024-12-31]');
// (
//   status:active AND deal_value:>10000
//   OR status:lead AND tags:enterprise
// )
// AND created:[2024-01-01 TO 2024-12-31]

Accepts a raw query string or a pre-parsed ASTNode. Options control line-break threshold and indentation:

import type { FormatQueryOptions } from 'elastic-input';

formatQuery(query, { maxLineLength: 80, indent: '\t' });

| Option | Type | Default | Description | |--------|------|---------|-------------| | maxLineLength | number | 60 | Lines shorter than this stay inline | | indent | string | ' ' (2 spaces) | Indent string per nesting level | | whitespaceOperator | string | — | Replace implicit AND (whitespace) with this operator (e.g. 'AND', '&&') |

Requirements

Runtime (Browser)

| Browser | Minimum Version | |---------|----------------| | Chrome | 85+ | | Firefox | 103+ | | Safari | 16.4+ | | Edge | 85+ (Chromium) |

The compiled output targets ES2018. Uses modern Range/Selection APIs for text insertion (no deprecated document.execCommand).

Build / Development

| Dependency | Minimum Version | |------------|----------------| | Node.js | 18.0.0+ | | React | 16.8.0+ (hooks) | | React DOM | 16.8.0+ |

These constraints are also declared in package.json via engines and browserslist.

No runtime dependencies beyond React/ReactDOM.

Development

yarn install
yarn dev          # Start demo dev server
yarn test         # Run tests
yarn test:watch   # Run tests in watch mode
yarn build        # Build library (ES + CJS)
yarn build:demo   # Build demo page

License

MIT