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 componentsDate values, calendar arithmetic, and time-zone-aware date handling.
- React Aria
(react-aria)· via shared Dethink componentsAccessibility hooks and overlay utilities used by the implementation.
- React Aria Components
(react-aria-components)· via shared Dethink componentsAccessible 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.
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.
| DT-412 | API latency spike on /search | blocked | Ada | 5 |
| DT-418 | Billing export drops tax lines | open | Lin | 3 |
| DT-421 | SSO login loops on Safari | open | Ada | 8 |
| DT-430 | Rate limit headers for the public API | open | Sam | 2 |
Show sourceexamples/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.
| Query | Means |
|---|---|
| status:open,blocked | Status is any of Open, Blocked |
| -status:done | Not (Status is Done) |
| status:!done | Status is none of Done |
| labels:&bug,api | Labels include all of Bug, API |
| estimate:>=5 estimate:!=3 | Comparisons: > >= < <= != |
| estimate:3..8 | Between 3 and 8, inclusive |
| created:>-7d | Created after 7 days ago |
| created:2026-09-01..today | Created between two days |
| created:last:30d | Created in the last 30 days |
| created:in:0w | Created this week (-1m is last month) |
| customer:yes | Yes/No fields take yes or no |
| title:^api title:="exact title" | Starts with, is |
| assignee:empty assignee:!empty | Is 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:5 | Any operator by id, for custom ones |
| Prop | What it does | Default |
|---|---|---|
fieldsFilterField[] | The same field schema as FilterBar. Field keys are what people type before the colon. | Not set |
stateFilterState | State from useFilterState. Pass the same state to a FilterBar to keep the text and the chips in sync. | Not set |
value / defaultValue / onValueChangeFilter | Controlled or uncontrolled filter when you don't pass state. onValueChange runs once per commit. | empty group |
defaultFieldstring | Field that plain words and quoted phrases search, with its default operator. | first text field |
maxDepthnumber | Group 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) => void | Called when a commit fails, with { code, message, start, end }. | Not set |
controlSize"sm" | "md" | "lg" | Matches the Input sizes. | "md" |
| Prop | What it does | Default |
|---|---|---|
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?)string | Canonical 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)Filter | Reuses 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.