|Kit

PillsInput

A multi-line input shell that lays out removable pills alongside a search field — the target MultiSelect and TagsInput mount into.

PillsInput is the input shell for multi-value fields. It wears the same chrome as Input — border, focus ring, invalid / disabled states — but wraps as values pile up, laying out Pills next to a search field. It's the target MultiSelect and TagsInput render into; reach for it directly when you're wiring your own multi-value control.

import { PillsInput, PillsInputField } from '@42/ui-react/pills-input';
import { Pill } from '@42/ui-react/pill';

<PillsInput>
  {values.map((v) => (
    <Pill key={v} withRemoveButton onRemove={() => remove(v)}>{v}</Pill>
  ))}
  <PillsInputField placeholder="Add a skill…" />
</PillsInput>

Presentational by design — it holds no value state. Drive the values yourself, or let MultiSelect / TagsInput do it.

Playground

PillsInput

Loading preview

Props

Code

<PillsInput size="md" placeholder="Add a tag…" />

Anatomy

Host Pills and a single PillsInputField inside. The shell sets the pill size and disabled through context, so the pills match the field with no extra props. PillsInputField grows to fill the current line and pops the last value on Backspace-when-empty.

'use client';
import { useState } from 'react';
import { PillsInput, PillsInputField } from '@42/ui-react/pills-input';
import { Pill } from '@42/ui-react/pill';

export function TagField() {
  const [values, setValues] = useState<string[]>(['c', 'unix']);
  const [search, setSearch] = useState('');

  const add = (raw: string) => {
    const v = raw.trim();
    if (v && !values.includes(v)) setValues((cur) => [...cur, v]);
    setSearch('');
  };

  return (
    <PillsInput>
      {values.map((v) => (
        <Pill key={v} withRemoveButton onRemove={() => setValues((cur) => cur.filter((x) => x !== v))}>
          {v}
        </Pill>
      ))}
      <PillsInputField
        value={search}
        placeholder={values.length === 0 ? 'Add a skill…' : ''}
        onChange={(e) => setSearch(e.currentTarget.value)}
        onKeyDown={(e) => {
          if (e.key === 'Enter') { e.preventDefault(); add(search); }
        }}
        onRemoveLast={() => setValues((cur) => cur.slice(0, -1))}
      />
    </PillsInput>
  );
}

Sizes & variants

size (xs–xl) and variant (default / filled / unstyled) match Input — try them in the Playground above. The shell grows in height as pills wrap, and the pills scale to the chosen size automatically. Here's the filled variant:

cunix42

Max lines

maxLines caps the pills at N rows; any beyond collapse into a static +N counter (measured, so it adapts to the field width). The convention is pills first, the field last — only the pills are capped; the field always stays visible. Since PillsInput has no dropdown, the hidden pills aren't individually removable while capped, so it's best when you also surface the full set elsewhere.

designuiuxreactvuesvelteangularsolidqwikember

Start & end slots

startSlot and endSlot flank the pills region with the same slot layout as Input — drop in a leading icon or a trailing control. They sit outside the wrapping pills, so a trailing control stays pinned as the pills wrap.

In Field

Like the rest of the family, wrap it in Field for a label, description, and error:

<Field label="Tags" description="Press Enter to add">
  <TagField />
</Field>

API

PillsInput

Prop

Type

PillsInputField

Prop

Type

PillsInputField forwards every other prop to the underlying <input> (value, onChange, placeholder, onKeyDown, …). PillsInput forwards to its root div (incl. asChild).

Accessibility

  • PillsInputField is a real <input> — label it via Field (or aria-label).
  • Backspace on the empty field removes the last value (onRemoveLast), matching the expected tag-field interaction; each pill's × is independently focusable and labelled.
  • RTL: the pills region is a wrapping flex row with logical start/end slots, so pills, field, and any adornments flow correctly under dir="rtl".

On this page