Skip to content
Dethink Components

ComponentsQueryInput

QueryInput

Type a filter instead of clicking it: status:open created:>-7d (assignee:ada OR labels:bug). The text and the chips edit the same filter, so people can switch between them without losing anything. Invalid text is underlined where it goes wrong and never changes the filter.

On this page

Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.

View registry files
  • Internationalized Date (@internationalized/date) · via shared Dethink components

    Date values, calendar arithmetic, and time-zone-aware date handling.

  • React Aria (react-aria) · via shared Dethink components

    Accessibility hooks and overlay utilities used by the implementation.

  • React Aria Components (react-aria-components) · via shared Dethink components

    Accessible interaction primitives, including keyboard behavior, focus handling, and ARIA semantics.

Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.

Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.

Import the component into your page or component file.

Usage
import {
  FilterBar,
  QueryInput,
  useFilterState,
} from "@dethink/components";

Type in the box, or edit a chip. Press Enter or leave the box to apply.

Text and chips on one filter

Start typing a field name for suggestions, then pick a value. Try labels:bug estimate:>=5, or customer:yes OR assignee:lin. Remove a chip and the text follows; press Escape twice to throw away an edit.

4 results
DT-412API latency spike on /searchblockedAda5
DT-418Billing export drops tax linesopenLin3
DT-421SSO login loops on SafariopenAda8
DT-430Rate limit headers for the public APIopenSam2
Show sourceexamples/query-input/issue-query.tsx
examples/query-input/issue-query.tsx
"use client";

import { useMemo } from "react";
import {
  DataTable,
  FilterBar,
  QueryInput,
  createFilter,
  createFilterCondition,
  createFilterPredicate,
  defineFilterFields,
  useFilterState,
  type DataTableColumnDef,
} from "@dethink/components";

type Issue = {
  id: string;
  title: string;
  status: "open" | "blocked" | "done";
  labels: string[];
  assignee: string;
  estimate: number;
  created: string;
  customer: boolean;
};

// A fixed "today" keeps relative dates identical on the server and in the
// browser. In an app, pass the request time instead.
const evaluateOptions = { now: Date.UTC(2026, 8, 30, 12) };

const fields = defineFilterFields<Issue>([
  { key: "title", label: "Title", type: "text" },
  {
    key: "status",
    label: "Status",
    type: "option",
    options: [
      { value: "open", label: "Open" },
      { value: "blocked", label: "Blocked" },
      { value: "done", label: "Done" },
    ],
  },
  {
    key: "labels",
    label: "Labels",
    type: "multiOption",
    options: [
      { value: "bug", label: "Bug" },
      { value: "api", label: "API" },
      { value: "auth", label: "Auth" },
      { value: "billing", label: "Billing" },
    ],
  },
  { key: "assignee", label: "Assignee", type: "text" },
  { key: "estimate", label: "Estimate", type: "number" },
  { key: "created", label: "Created", type: "date" },
  { key: "customer", label: "Customer reported", type: "boolean" },
]);

const issues: Issue[] = [
  {
    id: "DT-412",
    title: "API latency spike on /search",
    status: "blocked",
    labels: ["bug", "api"],
    assignee: "Ada",
    estimate: 5,
    created: "2026-09-29",
    customer: true,
  },
  {
    id: "DT-418",
    title: "Billing export drops tax lines",
    status: "open",
    labels: ["bug", "billing"],
    assignee: "Lin",
    estimate: 3,
    created: "2026-09-24",
    customer: true,
  },
  {
    id: "DT-421",
    title: "SSO login loops on Safari",
    status: "open",
    labels: ["bug", "auth"],
    assignee: "Ada",
    estimate: 8,
    created: "2026-09-12",
    customer: false,
  },
  {
    id: "DT-430",
    title: "Rate limit headers for the public API",
    status: "open",
    labels: ["api"],
    assignee: "Sam",
    estimate: 2,
    created: "2026-09-28",
    customer: false,
  },
  {
    id: "DT-433",
    title: "Invoice PDF uses the wrong locale",
    status: "done",
    labels: ["billing"],
    assignee: "Lin",
    estimate: 1,
    created: "2026-08-30",
    customer: true,
  },
];

const columns: DataTableColumnDef<Issue>[] = [
  { accessorKey: "id", header: "ID" },
  { accessorKey: "title", header: "Title" },
  { accessorKey: "status", header: "Status" },
  { accessorKey: "assignee", header: "Assignee" },
  { accessorKey: "estimate", header: "Estimate" },
];

export function QueryInputIssueQuery() {
  // One state drives both the text and the chips.
  const state = useFilterState({
    defaultValue: createFilter({
      id: "issues",
      children: [
        createFilterCondition({
          id: "status",
          field: "status",
          operator: "isAnyOf",
          value: ["open", "blocked"],
        }),
        createFilterCondition({
          id: "created",
          field: "created",
          operator: "inLast",
          value: { amount: 30, unit: "day" },
        }),
      ],
    }),
  });
  const rowFilter = useMemo(
    () => createFilterPredicate(state.filter, fields, evaluateOptions),
    [state.filter],
  );

  return (
    <DataTable
      aria-label="Issues"
      columns={columns}
      data={issues}
      getRowId={(row) => row.id}
      rowFilter={rowFilter}
      emptyContent="No issues match this query."
      toolbar={
        <div className="grid w-full gap-3">
          <QueryInput fields={fields} state={state} defaultField="title" />
          <FilterBar
            fields={fields}
            state={state}
            data={issues}
            evaluateOptions={evaluateOptions}
          />
        </div>
      }
    />
  );
}

Field keys go before the colon, then an optional operator token, then the value. Quote values with spaces or punctuation.

QueryMeans
status:open,blockedStatus is any of Open, Blocked
-status:doneNot (Status is Done)
status:!doneStatus is none of Done
labels:&bug,apiLabels include all of Bug, API
estimate:>=5 estimate:!=3Comparisons: > >= < <= !=
estimate:3..8Between 3 and 8, inclusive
created:>-7dCreated after 7 days ago
created:2026-09-01..todayCreated between two days
created:last:30dCreated in the last 30 days
created:in:0wCreated this week (-1m is last month)
customer:yesYes/No fields take yes or no
title:^api title:="exact title"Starts with, is
assignee:empty assignee:!emptyIs empty, is not empty
login "rate limit"Plain words search the default field
a b OR c(a AND b) OR c: AND binds tighter
a (b OR c)Parentheses group, up to 3 levels
estimate:gte:5Any operator by id, for custom ones
QueryInput props
PropWhat it doesDefault
fieldsFilterField[]The same field schema as FilterBar. Field keys are what people type before the colon.Not set
stateFilterStateState from useFilterState. Pass the same state to a FilterBar to keep the text and the chips in sync.Not set
value / defaultValue / onValueChangeFilterControlled or uncontrolled filter when you don't pass state. onValueChange runs once per commit.empty group
defaultFieldstringField that plain words and quoted phrases search, with its default operator.first text field
maxDepthnumberGroup levels allowed, counting the root as 1. Deeper parentheses are an error.3
labelsPartial<QueryInputLabels>Accessible names, placeholder, suggestion kinds and an error formatter that receives the error code and range.English
onQueryError(error: FilterQueryError) => voidCalled when a commit fails, with { code, message, start, end }.Not set
controlSize"sm" | "md" | "lg"Matches the Input sizes."md"
Text query core
PropWhat it doesDefault
parseFilterQuery(text, fields, options?){ ok: true; filter } | { ok: false; error }Parses text into a normalized filter. Errors carry a code, a message and the exact character range.Not set
printFilterQuery(filter, fields, options?)stringCanonical text for a filter. Parsing it gives back normalizeFilter(filter). Incomplete conditions are left out.Not set
getFilterQuerySuggestions(text, caret, fields)FilterQuerySuggestion[]Field, operator and value completions at the caret, each with the range it replaces.Not set
getFilterQuerySegments(text, fields)FilterQuerySegment[]Highlight ranges for your own query UI.Not set
reconcileFilterIds(next, previous)FilterReuses ids of unchanged nodes so a rebuilt filter keeps chip identity.Not set

The box is an ARIA combobox with a list of suggestions. ArrowDown and ArrowUp move through suggestions, Enter picks one (or applies the query when none is active), and Escape closes the list, then throws away the edit. Suggestions keep focus in the box.

Colors come from an overlay that is hidden from assistive tech; screen readers read the plain text. When a query can’t be applied the box is marked invalid, the message is announced and linked as its description, and the problem range is underlined.