> For AI agents: the complete documentation index is available at https://wallarm.github.io/design-system/llms.txt, the full documentation bundle is available at https://wallarm.github.io/design-system/llms-full.txt.

# FilterInput

A config-driven query builder — `fields` metadata in, an `ExprNode` tree out of `onChange` — for filtering a data-dense resource. Reach for a row of `Select`s instead when three or four facets picked by equality would do; reach for this when the attribute count is high, or the query needs AND/OR, grouping, or operators like `between` and `like`. The menus, chips and connectors are internal, so what you own is the field config.

[Source](https://github.com/wallarm/design-system/tree/main/packages/design-system/src/components/FilterInput) · [Storybook](https://wallarm.github.io/design-system/storybook/?path=/docs/patterns-filterinput-filterinput--overview)

```tsx
import { FilterInput } from '@wallarm-org/design-system/FilterInput';

<FilterInput fields={attackVectorFields} placeholder="Type to filter..." />
```

## Usage

```tsx
import { FilterInput } from '@wallarm-org/design-system/FilterInput';
```

## Examples

### FilterInput

#### With keyboard hint

`showKeyboardHint` prints the shortcut hint beside the input — worth turning on where filtering is new to the reader, and off where it is routine.

```tsx
import { FilterInput } from '@wallarm-org/design-system/FilterInput';

<FilterInput fields={attackVectorFields} placeholder="Type to filter..." showKeyboardHint />
```

#### With field groups

`fieldGroups` sorts the field menu under labelled headers, which is what keeps a fifty-field schema navigable.

```tsx
import { FilterInput } from '@wallarm-org/design-system/FilterInput';

<FilterInput fields={groupedDemoFields} fields={['attack_type'} />
```

#### With described chip

A field with a `description` carries it into a tooltip on the chip's first segment, so a committed filter can still explain itself; the operator and value segments keep click-to-edit.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode } from '@wallarm-org/design-system/FilterInput';

export const WithDescribedChip = () => {
    const [expression, setExpression] = useState<ExprNode | null>({
      type: 'condition',
      field: 'status_code',
      operator: '=',
      value: '200',
    });

    return (
      <FilterInput
        data-testid='filter-input'
        fields={describedChipFields}
        value={expression}
        onChange={setExpression}
        placeholder='Type to filter...'
      />
    );
  };
```

#### Error empty

`error` reddens the input as a whole — pair it with an `Alert` that says what was rejected, since the input itself cannot.

```tsx
import { FilterInput } from '@wallarm-org/design-system/FilterInput';

<FilterInput fields={attackVectorFields} placeholder="Type to filter..." error />
```

#### With preset value

Controlled mode: `value` takes an `ExprNode` and `onChange` hands the tree back. The JSON below the input is the story printing that tree, not part of the component.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode } from '@wallarm-org/design-system/FilterInput';

export const WithPresetValue = () => {
    const [expression, setExpression] = useState<ExprNode | null>({
      type: 'condition',
      field: 'status',
      operator: '=',
      value: 'Blocked',
    });

    return (
      <>
        <FilterInput
          data-testid='filter-input'
          fields={attackVectorFields}
          value={expression}
          onChange={setExpression}
          placeholder='Type to filter...'
        />
        {expression && (
          <div className='mt-16 rounded-8 border border-border-primary bg-bg-light-primary p-8 font-mono text-xs text-text-secondary'>
            <pre>{JSON.stringify(expression, null, 2)}</pre>
          </div>
        )}
      </>
    );
  };
```

#### With multi condition preset

Two conditions joined by AND — what a `group` node looks like once rendered, connectors included.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode } from '@wallarm-org/design-system/FilterInput';

export const WithMultiConditionPreset = () => {
    const [expression, setExpression] = useState<ExprNode | null>({
      type: 'group',
      operator: 'and',
      children: [
        { type: 'condition', field: 'status', operator: '=', value: 'Blocked' },
        { type: 'condition', field: 'status_code', operator: '>', value: 400 },
      ],
    });

    return (
      <>
        <FilterInput
          data-testid='filter-input'
          fields={attackVectorFields}
          value={expression}
          onChange={setExpression}
          placeholder='Type to filter...'
        />
        {expression && (
          <div className='mt-16 rounded-8 border border-border-primary bg-bg-light-primary p-8 font-mono text-xs text-text-secondary'>
            <pre>{JSON.stringify(expression, null, 2)}</pre>
          </div>
        )}
      </>
    );
  };
```

#### Error with value

The error state with a condition already committed: the chip stays editable, so the query can be fixed in place rather than cleared.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode } from '@wallarm-org/design-system/FilterInput';

export const ErrorWithValue = () => {
    const [expression, setExpression] = useState<ExprNode | null>({
      type: 'condition',
      field: 'status',
      operator: '=',
      value: 'Blocked',
    });

    return (
      <FilterInput
        data-testid='filter-input'
        fields={attackVectorFields}
        value={expression}
        onChange={setExpression}
        placeholder='Type to filter...'
        error
      />
    );
  };
```

#### With disabled chips

A condition marked `disabled` is locked — dimmed, no edit, no remove — which is how a drill-down carries its context into the filter without letting the reader drop it.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode } from '@wallarm-org/design-system/FilterInput';

export const WithDisabledChips = () => {
    const [expression, setExpression] = useState<ExprNode | null>({
      type: 'group',
      operator: 'and',
      children: [
        { type: 'condition', field: 'country', operator: '=', value: 'US', disabled: true },
        { type: 'condition', field: 'status', operator: '=', value: 'Blocked', disabled: true },
        { type: 'condition', field: 'status_code', operator: '>', value: 400 },
      ],
    });

    return (
      <>
        <FilterInput
          data-testid='filter-input'
          fields={attackVectorFields}
          value={expression}
          onChange={setExpression}
          placeholder='Add more filters...'
        />
        {expression && (
          <div className='mt-16 rounded-8 border border-border-primary bg-bg-light-primary p-8 font-mono text-xs text-text-secondary'>
            <pre>{JSON.stringify(expression, null, 2)}</pre>
          </div>
        )}
      </>
    );
  };
```

#### All chips disabled

With every chip locked, clear-all has nothing to remove, and the input still accepts new conditions on top.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode } from '@wallarm-org/design-system/FilterInput';

export const AllChipsDisabled = () => {
    const [expression, setExpression] = useState<ExprNode | null>({
      type: 'group',
      operator: 'and',
      children: [
        { type: 'condition', field: 'status', operator: '=', value: 'Monitoring', disabled: true },
        {
          type: 'condition',
          field: 'country',
          operator: 'in',
          value: ['US', 'DE'],
          disabled: true,
        },
      ],
    });

    return (
      <FilterInput
        data-testid='filter-input'
        fields={attackVectorFields}
        value={expression}
        onChange={setExpression}
        placeholder='Add more filters...'
      />
    );
  };
```

#### HTTP status code suggestions

A status-code field wired by hand with the `createStatusCode*` helpers: typing narrows the mask suggestions, 4 to 4XX and 40 to 40X.

```tsx
import { FilterInput, createStatusCodeInputFilter, createStatusCodeNormalizer, createStatusCodeSuggestions, createStatusCodeValidator } from '@wallarm-org/design-system/FilterInput';

<FilterInput label="Status code" type="integer" getSuggestions={createStatusCodeSuggestions()} validate={createStatusCodeValidator()} acceptChar={createStatusCodeInputFilter()} normalize={createStatusCodeNormalizer()} />
```

#### HTTP status code by name

`status_code` is a reserved field name — the DS fills in the same suggestions, input filter, normaliser and validator on its own, and an explicit callback still wins.

```tsx
import { FilterInput } from '@wallarm-org/design-system/FilterInput';

<FilterInput label="Status code" type="integer" operators={['='} />
```

#### Paired field

`pairedField` captures two steps in one chip — a parameter name, then its value with its own operator — for an attribute that means nothing on its own.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode } from '@wallarm-org/design-system/FilterInput';

export const PairedField = () => {
    const ParentComponent = () => {
      const [value, setValue] = useState<ExprNode | null>(null);
      return (
        <FilterInput
          data-testid='filter-input'
          fields={pairedFields}
          value={value}
          onChange={setValue}
        />
      );
    };
    return <ParentComponent />;
  };
```

#### Paired field value is set

`is set` and `is not set` on the paired half finish the chip with no second value, which is how you ask whether a key is present at all.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode } from '@wallarm-org/design-system/FilterInput';

export const PairedFieldValueIsSet = () => {
    const ParentComponent = () => {
      const [value, setValue] = useState<ExprNode | null>({
        type: 'group',
        operator: 'and',
        children: [
          { type: 'condition', field: 'context_param', operator: '=', value: 'header' },
          { type: 'condition', field: 'context_value', operator: 'is_null', value: null },
        ],
      });
      return (
        <FilterInput
          data-testid='filter-input'
          fields={pairedFields}
          value={value}
          onChange={setValue}
        />
      );
    };
    return <ParentComponent />;
  };
```

#### Paired field value descriptions

The value rows carry a muted second line because the bold name is not unique: two `request-id` rows differ only by their path, and selecting one commits the path.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode } from '@wallarm-org/design-system/FilterInput';

export const PairedFieldValueDescriptions = () => {
    const ParentComponent = () => {
      const [value, setValue] = useState<ExprNode | null>(null);
      return (
        <FilterInput
          data-testid='filter-input'
          fields={describedParamFields}
          value={value}
          onChange={setValue}
        />
      );
    };
    return <ParentComponent />;
  };
```

#### Paired field preset

The same paired field arriving pre-filled from a controlled value, rendered as the single chip it becomes.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode } from '@wallarm-org/design-system/FilterInput';

export const PairedFieldPreset = () => {
    const ParentComponent = () => {
      const [value, setValue] = useState<ExprNode | null>({
        type: 'condition',
        field: 'context_param',
        operator: '=',
        value: 'header',
        pair: { operator: '=', value: 'authorization' },
      });
      return (
        <FilterInput
          data-testid='filter-input'
          fields={pairedFields}
          value={value}
          onChange={setValue}
        />
      );
    };
    return <ParentComponent />;
  };
```

#### Nested value submenu

Values grouped under section headers, with one row opening a submenu of sub-types: only leaves commit, so the expression never carries a group. A fully selected group collapses to its own label in the chip while a partial one lists the chosen leaves, and typing flattens the tree to matches at any depth.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode } from '@wallarm-org/design-system/FilterInput';

export const NestedValueSubmenu = () => {
    const ParentComponent = () => {
      const [value, setValue] = useState<ExprNode | null>(null);
      return (
        <>
          <FilterInput
            data-testid='filter-input'
            fields={attackTypeNestedFields}
            value={value}
            onChange={setValue}
            placeholder='Filter by attack type…'
          />
          {value && (
            <div className='mt-16 rounded-8 border border-border-primary bg-bg-light-primary p-8 font-mono text-xs text-text-secondary'>
              <pre>{JSON.stringify(value, null, 2)}</pre>
            </div>
          )}
        </>
      );
    };
    return <ParentComponent />;
  };
```

### FilterInputChip

#### Default

The three segments a chip is made of; inside `FilterInput` each one is separately clickable, which is how a committed filter gets edited.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const Default = () => (
  <FilterInputChip />
);
```

#### With error

`error` reddens the chip for a value that was rejected. It is presentational only — the expression underneath is unchanged.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const WithError = () => (
  <FilterInputChip />
);
```

#### With long text

Long text in both segments. The chip caps at 320px; segments size to their content, with the attribute capped at 180px and the value guaranteed 60px minimum. When space is tight, the attribute shrinks first (since the value is what the filter matches on), both truncating with ellipsis rather than one segment collapsing to zero.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const WithLongText = () => (
  <FilterInputChip />
);
```

#### With value max width

A field with its own `valueMaxWidth` (here 500px) lifts the chip's blanket cap and lets the value segment run wide, truncating with an ellipsis only past that width — for long freeform values like parameter-path expressions (AS-1064). The full value is preserved for matching.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const WithValueMaxWidth = () => (
  <FilterInputChip />
);
```

#### Realistic example

The same chip carrying real content, which is where the segment widths actually get tested.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const RealisticExample = () => (
  <FilterInputChip />
);
```

#### And operator

The AND connector is itself a chip, clickable to change the join. Standalone it reads `FilterInput` context, so the story wraps it in a no-op provider.

```tsx
import { FilterInputConnectorChip } from '../FilterInputField';
import { MockFilterInputProvider } from './mockFilterInputContext';

export const AndOperator = () => (
  <MockFilterInputProvider>
    <FilterInputConnectorChip variant='and' chipId='c-1' onChange={() => undefined} />
  </MockFilterInputProvider>
);
```

#### Or operator

The OR connector, same shape — which is why AND/OR needs no separate control.

```tsx
import { FilterInputConnectorChip } from '../FilterInputField';
import { MockFilterInputProvider } from './mockFilterInputContext';

export const OrOperator = () => (
  <MockFilterInputProvider>
    <FilterInputConnectorChip variant='or' chipId='c-1' onChange={() => undefined} />
  </MockFilterInputProvider>
);
```

#### Combined with and

Two conditions and the connector between them: what the input assembles as each chip commits.

```tsx
import { FilterInputChip, FilterInputConnectorChip } from '../FilterInputField';
import { MockFilterInputProvider } from './mockFilterInputContext';

export const CombinedWithAnd = () => (
  <MockFilterInputProvider>
    <div className='flex items-center gap-4'>
      <FilterInputChip attribute='IP Address' operator='is' value='192.168.1.1' />
      <FilterInputConnectorChip variant='and' chipId='c-1' onChange={() => undefined} />
      <FilterInputChip attribute='Country' operator='is' value='US' />
    </div>
  </MockFilterInputProvider>
);
```

#### Combined with or

The same pair joined by OR, where the connector is the only difference between two very different queries.

```tsx
import { FilterInputChip, FilterInputConnectorChip } from '../FilterInputField';
import { MockFilterInputProvider } from './mockFilterInputContext';

export const CombinedWithOr = () => (
  <MockFilterInputProvider>
    <div className='flex items-center gap-4'>
      <FilterInputChip attribute='Status' operator='is' value='Active' />
      <FilterInputConnectorChip variant='or' chipId='c-1' onChange={() => undefined} />
      <FilterInputChip attribute='Status' operator='is' value='Pending' />
    </div>
  </MockFilterInputProvider>
);
```

#### With delete button

`onRemove` reveals the remove button on hover, so a resting filter row stays quiet.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const WithDeleteButton = () => (
  <FilterInputChip />
);
```

#### Error with delete

Error plus remove — a rejected chip has to stay removable, since dropping it is usually the fastest fix.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const ErrorWithDelete = () => (
  <FilterInputChip />
);
```

#### Interactive delete example

Removing chips one at a time, with the connectors closing up behind them.

```tsx
import * as React from 'react';
import { FilterInputChip, FilterInputConnectorChip } from '../FilterInputField';
import { MockFilterInputProvider } from './mockFilterInputContext';

export const InteractiveDeleteExample = () => {
  const [chips, setChips] = React.useState([
    { id: 1, attribute: 'IP Address', operator: 'is', value: '192.168.1.1' },
    { id: 2, attribute: 'Country', operator: 'is', value: 'US' },
    { id: 3, attribute: 'Status', operator: 'is', value: 'Active' },
  ]);

  return (
    <MockFilterInputProvider>
      <div className='flex items-center gap-4 flex-wrap'>
        {chips.map((chip, index) => (
          <React.Fragment key={chip.id}>
            {index > 0 && (
              <FilterInputConnectorChip
                variant='and'
                chipId={`connector-${chip.id}`}
                onChange={() => undefined}
              />
            )}
            <FilterInputChip
              data-testid='filter-input-chip'
              attribute={chip.attribute}
              operator={chip.operator}
              value={chip.value}
              onRemove={() => setChips(chips.filter(c => c.id !== chip.id))}
            />
          </React.Fragment>
        ))}
        {chips.length === 0 && <p className='text-text-secondary text-sm'>All filters removed</p>}
      </div>
    </MockFilterInputProvider>
  );
};
```

#### Disabled

`disabled` locks a chip: dimmed, no click, no remove, for a condition the surrounding page owns.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const Disabled = () => (
  <FilterInputChip />
);
```

#### Disabled with on remove

`disabled` wins over `onRemove` — no remove button appears, so a locked chip cannot be dropped by accident.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const DisabledWithOnRemove = () => (
  <FilterInputChip />
);
```

#### Disabled and interactive mix

Locked and editable chips in one row, which is what a drill-down actually looks like.

```tsx
import { FilterInputChip, FilterInputConnectorChip } from '../FilterInputField';
import { MockFilterInputProvider } from './mockFilterInputContext';

export const DisabledAndInteractiveMix = () => (
  <MockFilterInputProvider>
    <div className='flex items-center gap-4'>
      <FilterInputChip
        attribute='IP Address'
        operator='is'
        value='34.74.73.20'
        disabled
        onRemove={() => undefined}
      />
      <FilterInputConnectorChip variant='and' chipId='c-1' onChange={() => undefined} />
      <FilterInputChip attribute='Country' operator='is' value='US' onRemove={onRemove} />
    </div>
  </MockFilterInputProvider>
);
```

#### Building attribute only

`building` is the half-made chip shown while a condition is being composed — attribute chosen, operator next.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const BuildingAttributeOnly = () => (
  <FilterInputChip building attribute='IP Address' />
);
```

#### Building with operator

One step on, waiting for a value.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const BuildingWithOperator = () => (
  <FilterInputChip building attribute='IP Address' operator='is' />
);
```

#### Building complete

All three segments filled, at the point where the chip commits and stops building.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const BuildingComplete = () => (
  <FilterInputChip building attribute='IP Address' operator='is' value='192.168.1.1' />
);
```

#### All states showcase

Every chip state in one frame — the quickest way to check a visual change has not shifted one of them.

```tsx
import { FilterInputChip, FilterInputConnectorChip } from '../FilterInputField';
import { MockFilterInputProvider } from './mockFilterInputContext';

export const AllStatesShowcase = () => (
  <MockFilterInputProvider>
    <div className='flex flex-col gap-4'>
      {/* Chip variants */}
      <div>
        <div className='flex items-center gap-2 flex-wrap'>
          <FilterInputChip attribute='Attribute' operator='operator' value='Value' />
          <FilterInputChip attribute='Attribute' operator='operator' value='Value' error />
          <FilterInputChip
            attribute='Attribute'
            operator='operator'
            value='Value'
            onRemove={() => undefined}
          />
          <FilterInputChip
            attribute='Attribute'
            operator='operator'
            value='Value'
            error
            onRemove={() => undefined}
          />
        </div>
      </div>
      {/* Disabled chip variants */}
      <div>
        <div className='flex items-center gap-2 flex-wrap'>
          <FilterInputChip attribute='IP Address' operator='is' value='34.74.73.20' disabled />
          <FilterInputChip
            attribute='Host'
            operator='is'
            value='api.example.com'
            disabled
            onRemove={() => undefined}
          />
        </div>
      </div>
      {/* Building chip variants */}
      <div>
        <div className='flex items-center gap-2 flex-wrap'>
          <FilterInputChip building attribute='IP Address' />
          <FilterInputChip building attribute='IP Address' operator='is' />
          <FilterInputChip building attribute='IP Address' operator='is' value='192.168.1.1' />
        </div>
      </div>
      {/* Connector variants */}
      <div>
        <div className='flex items-center gap-2 flex-wrap'>
          <FilterInputConnectorChip variant='and' chipId='c-1' onChange={() => undefined} />
          <FilterInputConnectorChip variant='or' chipId='c-2' onChange={() => undefined} />
        </div>
      </div>
    </div>
  </MockFilterInputProvider>
);
```

#### Paired

A paired chip carries two triplets separated by a semicolon, for a field whose value means nothing without its key.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const Paired = () => (
  <FilterInputChip
    attribute='Context Param'
    operator='is'
    value='xxx'
    pair={{ attribute: 'Value', operator: 'is', value: 'yyy' }}
    onRemove={() => undefined}
  />
);
```

#### Paired with error

The error lands on the half that is wrong — here the required second value — rather than reddening the whole chip.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const PairedWithError = () => (
  <FilterInputChip
    attribute='Context Param'
    operator='is'
    value='xxx'
    pair={{ attribute: 'Value', operator: 'is', value: 'yyy', error: 'value' }}
    onRemove={() => undefined}
  />
);
```

#### Paired with long text

The same cap, 380px across two triplets. The base value is held to 90px so it cannot hide its partner, but every other segment keeps its full width, so the second value is the one that gives way.

```tsx
import { FilterInputChip } from '@wallarm-org/design-system/FilterInput';

export const PairedWithLongText = () => (
  <FilterInputChip
    attribute='Context Param With A Long Name'
    operator='is'
    value='a-very-long-first-value-that-truncates'
    pair={{ attribute: 'Value', operator: 'is', value: 'a-very-long-second-value-that-truncates' }}
    onRemove={() => undefined}
  />
);
```

### Composition

#### Default

The raw backend payload rendered as-is: around fifty fields, a hundred-plus application list, and the paired context-parameter field — the breadth the pattern has to survive.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode } from '@wallarm-org/design-system/FilterInput';
import { backendFieldsToMetadata, realBackendFields } from './backendFieldsFixture';

export const Default = () => {
    const [expression, setExpression] = useState<ExprNode | null>(null);

    const fields = realBackendFields.map(backendFieldsToMetadata);

    return (
      <>
        <FilterInput
          data-testid='filter-input'
          fields={fields}
          value={expression}
          onChange={setExpression}
          placeholder='Filter attacks...'
          showKeyboardHint
        />

        {/* Debug output */}
        {expression && (
          <div className='mt-16 rounded-8 border border-border-primary bg-bg-light-primary p-8 font-mono text-xs text-text-secondary'>
            <pre>{JSON.stringify(expression, null, 2)}</pre>
          </div>
        )}
      </>
    );
  };
```

#### Suggestions only

With `strictValues: false` everywhere, option lists become suggestions and any typed value commits, which is how the Attacks page treats the schema. Type checks and explicit `validate` callbacks still apply.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode, type FieldMetadata } from '@wallarm-org/design-system/FilterInput';

export const SuggestionsOnly = () => {
    const [expression, setExpression] = useState<ExprNode | null>(null);

    const suggestionFields: FieldMetadata[] = attackFields.map(field => ({
      ...field,
      strictValues: false,
    }));

    return (
      <>
        <FilterInput
          data-testid='filter-input'
          fields={suggestionFields}
          value={expression}
          onChange={setExpression}
          placeholder='Type to filter — options are suggestions, free text allowed...'
          showKeyboardHint
        />

        {expression && (
          <div className='mt-16 rounded-8 border border-border-primary bg-bg-light-primary p-8 font-mono text-xs text-text-secondary'>
            <pre>{JSON.stringify(expression, null, 2)}</pre>
          </div>
        )}
      </>
    );
  };
```

#### Simple

Two fields, to show the same component scales down — though at this size a pair of `Select`s is usually the better answer.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode, type FieldMetadata } from '@wallarm-org/design-system/FilterInput';

export const Simple = () => {
    const [expression, setExpression] = useState<ExprNode | null>(null);

    const simpleFields: FieldMetadata[] = [
      {
        name: 'status',
        label: 'Status',
        type: 'enum',
        values: [
          { value: 'active', label: 'Active' },
          { value: 'pending', label: 'Pending' },
          { value: 'archived', label: 'Archived' },
        ],
      },
      {
        name: 'priority',
        label: 'Priority',
        type: 'integer',
        values: [
          { value: 1, label: 'Low' },
          { value: 5, label: 'Medium' },
          { value: 10, label: 'High' },
        ],
      },
    ];

    return (
      <>
        <FilterInput
          data-testid='filter-input'
          fields={simpleFields}
          value={expression}
          onChange={expr => {
            onFilterChange(expr);
            setExpression(expr);
          }}
          placeholder='Filter items...'
        />

        {expression && (
          <div className='mt-16 rounded-8 border border-border-primary bg-bg-light-primary p-8 font-mono text-xs text-text-secondary'>
            <pre data-testid='expression-debug'>{JSON.stringify(expression, null, 2)}</pre>
          </div>
        )}
      </>
    );
  };
```

#### Backend integration

The schema arriving from a fetch: until `fields` exists there is nothing to filter by, so the input waits behind a placeholder block.

```tsx
import { useState } from 'react';
import { FilterInput, type ExprNode, type FieldMetadata } from '@wallarm-org/design-system/FilterInput';
import { Skeleton } from '@wallarm-org/design-system/Skeleton';

export const BackendIntegration = () => {
    const [metadata, setMetadata] = useState<FieldMetadata[] | null>(null);
    const [expression, setExpression] = useState<ExprNode | null>(null);

    // Simulate fetching metadata from backend
    useState(() => {
      // In real app: fetch('/api/security/query-metadata')
      setTimeout(() => {
        setMetadata(attackFields);
      }, 100);
    });

    if (!metadata) {
      return <Skeleton width='100%' height='40px' />;
    }

    return (
      <FilterInput
        data-testid='filter-input'
        fields={metadata}
        value={expression}
        onChange={setExpression}
        placeholder='Loading metadata from backend...'
      />
    );
  };
```

### FilterInputFieldMenu

#### Default

The plain list — every field in the config, label first, whatever its type.

```tsx
export const Default = () => <FieldMenuHarness fields={sampleFields} onSelect={noop} />;
```

#### Few fields

A short config, to check the menu does not pad itself out.

```tsx
export const FewFields = () => <FieldMenuHarness fields={sampleFields.slice(0, 5)} onSelect={noop} />;
```

#### Closed

`open={false}` renders nothing at all — no collapsed shell, no placeholder — so
this frame is deliberately empty. It is the one story with no trigger.

```tsx
<FilterInput fields={sampleFields} open={false} onSelect={noop} />
```

#### Interactive

Selection wired up, so you can watch which field the menu hands back.

```tsx
export const Interactive = () => <FieldMenuHarness fields={sampleFields} onSelect={noop} showSelection />;
```

#### With search

Typing filters the list by label and name, case-insensitively — try “status” or “CWE”.

```tsx
export const WithSearch = () => <FieldMenuHarness fields={sampleFields} onSelect={noop} />;
```

#### With recent fields

`recentConditions` puts the last few filters back at the top, capped at three —
what keeps a fifty-field menu usable day to day. Each row replays the whole
condition, not just the field.

```tsx
export const WithRecentFields = () => (
    <FieldMenuHarness fields={sampleFields} recentConditions={recentConditions} onSelect={noop} />
  );
```

#### With suggestions

A curated suggestions section, for the handful of fields most people reach for first.

```tsx
export const WithSuggestions = () => (
    <FieldMenuHarness
      fields={sampleFields}
      suggestedFields={sampleFields.slice(0, 3)}
      onSelect={noop}
    />
  );
```

#### With groups

Fields under labelled group headers, in array order, with anything left out of
every group falling into a trailing headerless section — here, `CWE`.

```tsx
export const WithGroups = () => (
    <FieldMenuHarness
      fields={sampleFields}
      fieldGroups={[
        { label: 'Threat classification', fields: ['status', 'severity', 'blocking_status'] },
        {
          label: 'Request features',
          fields: ['http_status_code', 'endpoint', 'hostname', 'parameter'],
        },
        { label: 'Source and identity', fields: ['location', 'network', 'impact'] },
      ]}
      onSelect={noop}
    />
  );
```

#### With descriptions

Hovering or keyboard-focusing a row opens a popover beside the menu with the description and, where the value format is not obvious, an example. A row with no description opens nothing, and group headers are inert.

```tsx
export const WithDescriptions = () => (
    <FieldMenuHarness
      fields={describedFields}
      fieldGroups={[
        {
          label: 'Threat classification',
          fields: ['attack_type', 'parameter', 'status_code', 'no_desc'],
        },
      ]}
      onSelect={noop}
    />
  );
```

#### With scrollable descriptions

A described list long enough to scroll, for checking the popover keeps up with the cursor and the keyboard.

```tsx
export const WithScrollableDescriptions = () => <FieldMenuHarness fields={manyDescribedFields} onSelect={noop} />;
```

#### With recent and suggestions

Both props set, and only Recent renders — the two shortcut sections are
mutually exclusive, so suggestions are the fallback for someone with no
history rather than a second row of shortcuts.

```tsx
export const WithRecentAndSuggestions = () => (
    <FieldMenuHarness
      fields={sampleFields}
      recentConditions={recentConditions.slice(0, 2)}
      suggestedFields={sampleFields.slice(0, 2)}
      onSelect={noop}
    />
  );
```

### FilterInputOperatorMenu

#### String type

A string field: equality, substring matching, set membership, and the two presence checks.

```tsx
export const StringType = () => <OperatorMenuHarness fieldType='string' />;
```

#### Integer type

Numbers add the comparison operators, which is the usual reason to reach for a query builder at all, with `in` below the rule as its own group. `between` is not here — it is date-only.

```tsx
export const IntegerType = () => <OperatorMenuHarness fieldType='integer' />;
```

#### Float type

Same comparisons as integer, minus `in`: picking an exact float out of a list is not a real query.

```tsx
export const FloatType = () => <OperatorMenuHarness fieldType='float' />;
```

#### Date type

Dates are the only type that offers `between`, so a time window is one condition rather than two, and the comparisons read as before and after.

```tsx
export const DateType = () => <OperatorMenuHarness fieldType='date' />;
```

#### Boolean type

A boolean offers is / is not and the presence checks, and nothing else is worth asking.

```tsx
export const BooleanType = () => <OperatorMenuHarness fieldType='boolean' />;
```

#### Enum type

An enum trades comparison for set membership: is one of / is not one of is the operator that earns its place here.

```tsx
export const EnumType = () => <OperatorMenuHarness fieldType='enum' />;
```

#### Interactive

Selection wired up, with the chosen operator echoed beside the trigger.

```tsx
export const Interactive = () => <OperatorMenuHarness fieldType='string' />;
```

#### Keyboard navigation

Arrow keys move, Enter selects, Escape closes — the menu is fully operable without a pointer once the trigger has opened it.

```tsx
export const KeyboardNavigation = () => (
  <div className='flex flex-col gap-8'>
    <OperatorMenuHarness fieldType='string' />
  </div>
);
```

## Props

| Prop             | Type                                                    | Default             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ---------------- | ------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| fields           | `FieldMetadata[] \| undefined`                          |                     | Filter-field configurations driving the autocomplete. A few names are **reserved** and auto-wired with design-system helpers — DS-supplied callbacks **override** consumer values for the reserved slots, because the field semantics (mask range, accepted chars, backend form) are fixed by DS: - `status_code` — HTTP status code field (mask suggestions, format validation, digit-or-X input filter, partial-input normalization). To opt out, use a different `name` and attach the factories (`createStatusCodeSuggestions`, …) manually. |
| fieldGroups      | `FieldGroup[] \| undefined`                             |                     | Optional grouping for the field-selection menu. When omitted, fields render as a flat list. When provided, fields render under labeled group headers (group order = array order; field order = listed order). Fields not referenced by any group fall into a trailing headerless section. Referenced by field `name`; unknown names are ignored.                                                                                                                                                                                                 |
| value            | `ExprNode \| null \| undefined`                         |                     |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| onChange         | `((expression: ExprNode \| null) => void) \| undefined` |                     |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| placeholder      | `string \| undefined`                                   | `Type to filter...` |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| error            | `boolean \| undefined`                                  | `false`             |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| externalErrors   | `string[] \| undefined`                                 |                     | Field names whose values were rejected by the backend. Matching chips are rendered with a value error (red). Purely presentational: conditions and `onChange` output are unaffected, and the consumer is expected to render its own message (e.g. an alert with the backend error text) and clear the prop when the filter changes or the query succeeds.                                                                                                                                                                                        |
| onErrorsChange   | `((errors: string[]) => void) \| undefined`             |                     | Notified whenever the set of validation messages FilterInput renders below the input changes (empty array = no visible error). Lets a consumer avoid stacking its own message (e.g. a backend-error alert) on top of one the input already shows. Pass a stable (memoized) callback.                                                                                                                                                                                                                                                             |
| showKeyboardHint | `boolean \| undefined`                                  | `false`             |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| title            | `string \| undefined`                                   |                     |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| defaultChecked   | `boolean \| undefined`                                  |                     |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| defaultValue     | `string \| number \| readonly string[] \| undefined`    |                     |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| autoFocus        | `boolean \| undefined`                                  |                     |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| id               | `string \| undefined`                                   |                     |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| tabIndex         | `number \| undefined`                                   |                     |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| rel              | `string \| undefined`                                   |                     |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| data-testid      | `string \| undefined`                                   |                     |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |

### FilterInputChip

| Prop                 | Type                                                 | Default | Description                                                                                                                                                                                                         |
| -------------------- | ---------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| chipId               | `string \| undefined`                                |         |                                                                                                                                                                                                                     |
| attribute \*         | `string`                                             |         |                                                                                                                                                                                                                     |
| attributeDescription | `string \| undefined`                                |         | Description shown in a dark tooltip on hover of the attribute segment. When absent, no tooltip renders. The operator/value segments are unaffected (AS-1060).                                                       |
| operator             | `string \| undefined`                                |         |                                                                                                                                                                                                                     |
| value                | `string \| undefined`                                |         |                                                                                                                                                                                                                     |
| error                | `ChipErrorSegment \| undefined`                      | `false` |                                                                                                                                                                                                                     |
| valueParts           | `string[] \| undefined`                              |         |                                                                                                                                                                                                                     |
| valueSeparator       | `string \| undefined`                                |         |                                                                                                                                                                                                                     |
| errorValueIndices    | `number[] \| undefined`                              |         |                                                                                                                                                                                                                     |
| building             | `boolean \| undefined`                               | `false` |                                                                                                                                                                                                                     |
| disabled             | `boolean \| undefined`                               | `false` | When true, the chip cannot be edited or removed (dimmed appearance)                                                                                                                                                 |
| valueMaxWidth        | `number \| undefined`                                | `500`   | Caps the applied value segment at this width (px), truncating with an ellipsis; lifts the chip's default max-width so it grows to fit. Defaults to 500; a paired chip caps its whole width there instead (AS-1064). |
| caretMode            | `"end" \| undefined`                                 | `end`   | Caret placement on value inline-edit: `'end'` (default) puts it at the tail — better for long values; pass explicitly to keep the API visible (AS-1064).                                                            |
| pair                 | `complex`                                            |         | Second paired triplet (two-step fields). The paired attribute is fixed.                                                                                                                                             |
| onRemove             | `(() => void) \| undefined`                          |         |                                                                                                                                                                                                                     |
| onSegmentClick       | `complex`                                            |         |                                                                                                                                                                                                                     |
| onPairSegmentClick   | `complex`                                            |         | Click on an editable paired segment (operator/value of the second triplet).                                                                                                                                         |
| title                | `string \| undefined`                                |         |                                                                                                                                                                                                                     |
| defaultChecked       | `boolean \| undefined`                               |         |                                                                                                                                                                                                                     |
| defaultValue         | `string \| number \| readonly string[] \| undefined` |         |                                                                                                                                                                                                                     |
| autoFocus            | `boolean \| undefined`                               |         |                                                                                                                                                                                                                     |
| id                   | `string \| undefined`                                |         |                                                                                                                                                                                                                     |
| tabIndex             | `number \| undefined`                                |         |                                                                                                                                                                                                                     |
| rel                  | `string \| undefined`                                |         |                                                                                                                                                                                                                     |
| onChange             | `ChangeEventHandler<T, Element> \| undefined`        |         |                                                                                                                                                                                                                     |
| data-testid          | `string \| undefined`                                |         |                                                                                                                                                                                                                     |

### FilterInputFieldMenu

| Prop             | Type                                               | Default | Description                                                             |
| ---------------- | -------------------------------------------------- | ------- | ----------------------------------------------------------------------- |
| fields \*        | `FieldMetadata[]`                                  |         |                                                                         |
| filterText       | `string \| undefined`                              |         | Text from the input to filter displayed fields                          |
| onSelect \*      | `(field: FieldMetadata) => void`                   |         |                                                                         |
| open             | `boolean \| undefined`                             | `false` |                                                                         |
| onOpenChange     | `((open: boolean) => void) \| undefined`           |         |                                                                         |
| recentConditions | `Condition[] \| undefined`                         | `[]`    |                                                                         |
| suggestedFields  | `FieldMetadata[] \| undefined`                     | `[]`    |                                                                         |
| fieldGroups      | `FieldGroup[] \| undefined`                        |         | Optional grouping for the field list. When omitted, fields render flat. |
| onSelectAnd      | `(() => void) \| undefined`                        |         |                                                                         |
| onSelectOr       | `(() => void) \| undefined`                        |         |                                                                         |
| onEscape         | `(() => void) \| undefined`                        |         |                                                                         |
| positioning      | `Record<string, unknown> \| undefined`             |         |                                                                         |
| inputRef         | `RefObject<HTMLInputElement \| null> \| undefined` |         | Ref to the query bar input — ArrowUp on first item returns focus here   |
| menuRef          | `RefObject<HTMLDivElement \| null> \| undefined`   |         | Ref to the menu content (shared across menus for focus management).     |
| data-testid      | `string \| undefined`                              |         |                                                                         |

### FilterInputOperatorMenu

| Prop         | Type                                               | Default | Description                                                                                                                                                                             |
| ------------ | -------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| fieldType \* | `FieldType`                                        |         | The field type to determine which operators to show                                                                                                                                     |
| operators    | `FilterOperator[] \| undefined`                    |         | Optional list of operators from field config. When provided, only these operators are shown (preserving type-based grouping). Falls back to OPERATORS_BY_TYPE\[fieldType] when not set. |
| onSelect \*  | `(operator: FilterOperator) => void`               |         | Callback when an operator is selected                                                                                                                                                   |
| open         | `boolean \| undefined`                             | `false` | Whether the menu is open                                                                                                                                                                |
| onOpenChange | `((open: boolean) => void) \| undefined`           |         | Callback when open state should change                                                                                                                                                  |
| onEscape     | `(() => void) \| undefined`                        |         | Callback when Escape is pressed (discard)                                                                                                                                               |
| positioning  | `Record<string, unknown> \| undefined`             |         | Override positioning config for the dropdown                                                                                                                                            |
| inputRef     | `RefObject<HTMLInputElement \| null> \| undefined` |         | Ref to the query bar input — ArrowUp on first item returns focus here                                                                                                                   |
| filterText   | `string \| undefined`                              |         | Text to filter operators by label                                                                                                                                                       |
| menuRef      | `RefObject<HTMLDivElement \| null> \| undefined`   |         | Ref to the menu content element — shared across menus for focus management                                                                                                              |
| data-testid  | `string \| undefined`                              |         |                                                                                                                                                                                         |

### FilterInputValueMenu

| Prop                  | Type                                                    | Default | Description                                                               |
| --------------------- | ------------------------------------------------------- | ------- | ------------------------------------------------------------------------- |
| values \*             | `ValueOption[]`                                         |         |                                                                           |
| onSelect \*           | `(value: ConditionValue) => void`                       |         |                                                                           |
| onCommit              | `((values: ConditionValue[]) => void) \| undefined`     |         |                                                                           |
| open                  | `boolean \| undefined`                                  |         |                                                                           |
| onOpenChange          | `((open: boolean) => void) \| undefined`                |         |                                                                           |
| multiSelect           | `boolean \| undefined`                                  |         |                                                                           |
| initialValues         | `ConditionValue[] \| undefined`                         |         |                                                                           |
| highlightValue        | `ConditionValue \| undefined`                           |         |                                                                           |
| onEscape              | `(() => void) \| undefined`                             |         |                                                                           |
| width                 | `number \| "standard" \| "compact" \| undefined`        |         |                                                                           |
| positioning           | `Record<string, unknown> \| undefined`                  |         |                                                                           |
| onBuildingValueChange | `((preview: string \| undefined) => void) \| undefined` |         |                                                                           |
| onItemToggle          | `(() => void) \| undefined`                             |         | Fires only on user-initiated multi-select toggle (not on init).           |
| inputRef              | `RefObject<HTMLInputElement \| null> \| undefined`      |         | Query bar input — ArrowUp on first item returns focus here.               |
| filterText            | `string \| undefined`                                   |         | Filter values by label.                                                   |
| loading               | `boolean \| undefined`                                  |         | Options are still loading — show a loading indicator instead of the list. |
| emptyLabel            | `string \| undefined`                                   |         | Replaces the empty-state's default "No results" text.                     |
| menuRef               | `RefObject<HTMLDivElement \| null> \| undefined`        |         | Menu content ref (shared across menus for focus management).              |
| blurCommitRef         | `RefObject<(() => boolean) \| null> \| undefined`       |         | Set here so blur handler can commit multi-select values.                  |
