Query
A single-line filter field for a list. Free text and key:value qualifiers mix in one query: is:shared -staging api. The field colors each token in place. A suggestion popover completes keys and values while the caret stays in the field.
A grammar from @ngrok/mantle/query-filter sets the vocabulary. The same grammar filters your items with filterItemsByQuery, so the field and the list always agree.
Type is: to see the values and their live match counts.
The query is the only state, and you own it through value and onValueChange. There's no uncontrolled form, because every list that hosts this field already keeps the query somewhere: in component state, in form state, or in the URL. To keep it in ?q=, follow the Query from the URL recipe.
Composition
Root lays out its children in one row, so put your own buttons beside the field there. Input is the field. Content is the suggestion popover; without it, the field is a highlighted input with no suggestions.
You compose the popover from its parts:
Listis the listbox. Its child is a function that gets each suggestion and returns aQuery.Itemfor it. Pass children toItemto show your own row content.Emptyshows when no suggestion matches. By default, it says what the field accepts at the caret. Pass children to show your own message.Footerstays under the rows while they scroll. PutLegend,KeyboardHints, and your own content, such as a link to your filter docs, in it.
Leave out a part to remove it. Wrap your own text in Footer in an element, such as a <p>, so it is not bare text beside the legend.
Polymorphism
Query.Root takes asChild. Use it to render the row as your own element, such as a <section> with its own label, while Root keeps the state and the layout classes.
Query.Footer takes asChild. Use it to render the footer as your own element, while the footer keeps its id and the input keeps it as its description.
The other parts don't take asChild:
Inputrenders the field's shell, its painted copy, and the<input>together.ContentwrapsPopover.Content, whose element Radix positions.Listrenders the section groups and their headings.Itemmust stay therole="option"element that the input names througharia-activedescendant.Empty,Legend, andKeyboardHintsrender their own content.
Inline actions and a footer
While the query has text, Query.Input shows a clear button at its right edge. The button empties the query and moves focus back to the field. Set clearLabel to rename it. Set onClear for work that only a clear does, such as applying the empty query at once. Other children of Query.Input render before the clear button. Pass ghost xs icon buttons.
Query.KeyboardHints shows the keys the field answers to. When Enter submits a form, set enterHint to name that action, such as "select, or search". Put your own footer content in Query.Footer, after the hints.
projectFilterGrammar and projects are the ones from the first example.
Search on the server
When a server runs the query, declare its vocabulary with defineRemoteFilterGrammar and leave out items. The suggestions then carry no counts. The grammar's syntax keeps the highlighting to what the server reads: "qualifiers" (the default) treats only bare key:value tokens as structure, and "conjunction" reads one AND group of exclusions, comma lists, and quotes.
Inside a <form>, Enter submits the form, unless the keyboard made a suggestion active. Then Enter accepts that suggestion.
Large lists
The match counts and the live filter each run one pass over items per keystroke. For thousands of rows, or a list that streams in, apply the query on Enter: wrap the field in a <form>, leave out items, and set enterHint on Query.KeyboardHints. Enter with no active suggestion submits the form. The Query from the URL recipe has the hook and a demo.
Filter from a value
To let a person select a value in a table row and get its filter, call toggleFacet in the click handler. The Filter from a value recipe has the demo.
Token colors
Keys, values, and OR take the colors that Code Block gives attributes, strings, and operators, so a query reads like the code on the same page. Set the three CSS variables on the field or any ancestor to change them. This calmer set colors only the keys:
Accessibility
The field is a real <input role="combobox"> with aria-autocomplete="list". The text you edit is the input's own value, so the caret, selection, IME composition, paste, and undo all stay native. A painted copy of the text sits over the input and is hidden from assistive tech.
- Focus never leaves the input. The active suggestion is announced through
aria-activedescendant. - No suggestion is active until an arrow key moves. Up and Down wrap around the list. A pointer over a row highlights it but doesn't arm Enter.
- Enter accepts the active suggestion. With no active suggestion, Enter closes the list and lets the event reach your form. Escape closes the list.
- A caret move retires the active suggestion, so Enter can't accept a row from a list you didn't see.
- While an IME composes, every key goes to the IME.
- While the popover is open,
Query.Footerjoins the input's accessible description, after any description you set. While no suggestion matches,Query.Emptyjoins it too.
Name the field with aria-label, or put it in a Field and label it with Field.Label. Inside Field.Control, the field takes its id, name, aria-describedby, aria-errormessage, and aria-invalid from the Field.Item.
The section headings ("Filter by", "Exclude", "Values"), the legend, the keyboard hints, and the default empty message are English copy that mantle sets. The listbox name defaults to "Filter suggestions"; set aria-label on Query.List to change it.
API Reference
Query.Root
The row and the state owner. It renders a div that lays out its children in a row, and accepts every div prop.
Query.Input
The query field. className and style go to the field's outer element, as on Input. All other props go to the <input>, such as ref, id, name, disabled, required, readOnly, validation, the aria-* props, and the event handlers. A disabled or read-only field shows no suggestions and no clear button.
Query.Content
The suggestion popover. It holds the parts you compose into it and keeps focus in the field. It accepts the Popover.Content props except the ones it owns: align, role, preferredWidth, the auto-focus handlers, and onInteractOutside.
Query.List
The suggestion listbox for the token under the caret. It groups the rows under "Filter by", "Exclude", and "Values", and it scrolls when the rows don't fit. It accepts every div prop except id and role.
Query.Item
One suggestion row: a role="option" that inserts its suggestion on a click. The arrow keys in the field make a row active, and Enter inserts it. It accepts every div prop except id, role, tabIndex, and aria-selected. Your event handlers run first; call preventDefault() in onClick to skip the insert.
Query.Empty
The message that shows in place of the rows when no suggestion matches. It accepts every div prop except id.
Query.Footer
The area under the rows, pinned while the rows scroll. It accepts every div prop except id. The footer sets its own id, so the field can name it as its description. With asChild, that id replaces the id of the child.
Query.Legend
The syntax reminder. It shows only the syntax the grammar honors. It accepts every p prop except children.
Query.KeyboardHints
The keys the field answers to: the arrows, Enter, and Escape. It accepts every p prop except children.