Redpanda UI
RC
Redpanda UI

Key Value Field

A form component for managing a dynamic list of key-value pairs with add and delete controls.

Made by eblairmckee
Loading component...

When to use

Use KeyValueField when users need to define arbitrary metadata as key-value pairs — for example, resource labels, environment variables, or HTTP headers. It manages a dynamic list where items can be freely added, edited, and removed.

Installation

Usage

import { KeyValueField, type KeyValuePair } from '@/registry/components/key-value-field';
const [pairs, setPairs] = useState<KeyValuePair[]>([]);

<KeyValueField
  label="Labels"
  onChange={setPairs}
  value={pairs}
/>

Examples

Default

Loading component...

Creatable Combobox

Loading component...

Form Integration

Use with React Hook Form via a controlled FormField:

import { KeyValueField, type KeyValuePair } from '@/registry/components/key-value-field';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/registry/components/form';

<FormField
  control={form.control}
  name="labels"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Labels</FormLabel>
      <FormControl>
        <KeyValueField
          onChange={field.onChange}
          value={field.value}
        />
      </FormControl>
      <FormMessage />
    </FormItem>
  )}
/>

Custom Placeholders

<KeyValueField
  keyFieldProps={{ placeholder: 'Header name' }}
  label="HTTP Headers"
  onChange={setHeaders}
  valueFieldProps={{ placeholder: 'Header value' }}
  value={headers}
/>

Combobox Fields

Use mode: 'combobox' on keyFieldProps or valueFieldProps to render a combobox with predefined options instead of a plain input. All ComboboxProps (such as creatable, options, and onCreateOption) are available.

<KeyValueField
  keyFieldProps={{
    mode: 'combobox',
    options: [
      { label: 'environment', value: 'environment' },
      { label: 'region', value: 'region' },
    ],
    creatable: true,
    placeholder: 'Select or create key',
  }}
  label="Labels"
  onChange={setPairs}
  value={pairs}
/>

Custom Add Button Label

<KeyValueField
  addButtonLabel="Add variable"
  label="Environment Variables"
  onChange={setEnvVars}
  value={envVars}
/>

Without Add Button

Use showAddButton={false} when you want to control the list externally or render the add control elsewhere.

<KeyValueField
  label="Labels"
  onChange={setPairs}
  showAddButton={false}
  value={pairs}
/>

Props

PropTypeDefaultDescription
valueKeyValuePair[][]Controlled list of key-value pairs
onChange(value: KeyValuePair[]) => voidCalled with the updated list on any change
labelReactNodeLabel rendered above the field
descriptionReactNodeHelper text rendered below the label
addButtonLabelstring'Add'Label for the add button
keyFieldPropsKeyValueFieldConfig{ placeholder: 'Key' }Props for each key field — see below
valueFieldPropsKeyValueFieldConfig{ placeholder: 'Value' }Props for each value field — see below
showAddButtonbooleantrueWhether to render the add button
disabledbooleanDisables all inputs and buttons
maxItemsnumberMaximum number of pairs allowed — hides the add button when reached
testIdstringBase data-testid — sub-IDs are derived automatically

KeyValuePair type

type KeyValuePair = {
  key: string;
  value: string;
};

KeyValueFieldConfig type

A discriminated union that configures each key/value field. Set mode to choose between a plain input and a combobox.

// Plain input (default)
type InputFieldConfig = { mode?: 'input' } & Omit<InputProps, 'value' | 'onChange' | 'disabled' | 'aria-invalid'>;

// Combobox with options
type ComboboxFieldConfig = { mode: 'combobox' } & Omit<ComboboxProps, 'value' | 'onChange' | 'disabled'>;

type KeyValueFieldConfig = InputFieldConfig | ComboboxFieldConfig;

Validation

The component applies inline validation styling automatically:

  • If a value is filled but the key is empty, the key input is marked aria-invalid.
  • If a key is filled but the value is empty, the value input is marked aria-invalid.
  • If two or more rows share the same key, all duplicate key inputs are marked aria-invalid.

For full form validation (required fields, custom rules), integrate with a form library such as React Hook Form and render <FormMessage /> alongside the field.

Focus management

After adding a row, focus automatically moves to the first input in the new row. After deleting a row, focus moves to the next row (or the previous row if the last row was deleted). This is powered by the useInputListFocus hook from input-utils.

Companion Utilities

The input-utils library provides utilities that pair well with KeyValueField.

keyValuePairsSchema

Returns a Zod schema that validates key-value pairs — covering empty keys, duplicate keys, character patterns, length limits, and item count. Use with React Hook Form + zodResolver for form-level validation.

Loading component...
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { keyValuePairsSchema } from '@/registry/lib/input-utils';

const FormSchema = z.object({
  labels: keyValuePairsSchema({ maxItems: 10, maxKeyLength: 128 }),
});

const form = useForm<z.infer<typeof FormSchema>>({
  resolver: zodResolver(FormSchema),
  defaultValues: { labels: [] },
});

The schema adds a root-level Zod issue (displayed by FormMessage) and per-field issues at [index, field] paths for row-level error access.

KeyValuePairsSchemaOptions

OptionTypeDescription
maxItemsnumberMaximum number of pairs allowed
maxKeyLengthnumberMaximum character length for keys
maxValueLengthnumberMaximum character length for values
allowedPatternRegExpRegex that keys and values must match

getKeyValueDiff

Compares initial and current pairs to produce a delta of created, updated, and removed entries — useful when submitting only changes to an API.

Loading component...
import { getKeyValueDiff } from '@/registry/lib/input-utils';

const diff = getKeyValueDiff(initialPairs, currentPairs);
// diff.created  → { newKey: 'value' }
// diff.updated  → { existingKey: 'newValue' }
// diff.removed  → ['deletedKey']

useMemoizedArray

Preserves object references for unchanged items across renders, reducing unnecessary child re-renders when the parent rebuilds the array.

import { useMemoizedArray } from '@/registry/lib/input-utils';

const stablePairs = useMemoizedArray(
  pairs,
  (a, b) => a.key === b.key && a.value === b.value,
);

useUndoRemoval

Soft-delete pattern with timed auto-confirm and undo support. Call markForRemoval with a string identifier and a callback that fires after the timeout unless undoRemoval is called first.

Loading component...
import { useUndoRemoval } from '@/registry/lib/input-utils';

const { markForRemoval, undoRemoval, isPending } = useUndoRemoval(5000);

markForRemoval('item-id', () => {
  // Actually remove the item after 5 seconds
});

// Cancel within the timeout
undoRemoval('item-id');

// Check pending state for rendering
isPending('item-id'); // true while waiting

useInputListFocus

Manages focus after add/remove operations in dynamic input lists. Already integrated into KeyValueField — use directly when building your own dynamic list components.

import { useInputListFocus } from '@/registry/lib/input-utils';

const containerRef = useRef<HTMLDivElement>(null);
const { onAdd, onRemove } = useInputListFocus(containerRef);

// After appending a row:
onAdd(); // focuses the first input in the new row

// After deleting a row:
onRemove(deletedIndex); // focuses the next logical row

Rows must have a data-row attribute for the focus selector to work.

findDuplicateIndices

Generic utility that returns a Set<number> of indices whose extracted key appears more than once. Already used internally by KeyValueField for duplicate detection.

import { findDuplicateIndices } from '@/registry/lib/input-utils';

const duplicates = findDuplicateIndices(items, (item) => item.key);
// duplicates → Set { 1, 3 } if items[1] and items[3] share a key

Testing

When testId is provided, child elements receive derived IDs:

Elementdata-testid
Root containertestId
Key input at index ntestId-key-n
Value input at index ntestId-value-n
Delete button at index ntestId-delete-n
Add buttontestId-add

Built by malinskibeniamin. The source code is available on GitHub.

On this page