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/TagsInputdo it.
Playground
PillsInput
Dir
Presets
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:
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.
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
PillsInputFieldis a real<input>— label it viaField(oraria-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".