DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

A chip input is a text box and a list fused into one control — built from one plain input and an array of strings

A chip input — the field behind every recipient box, tag picker and "add skills" form — looks like a bespoke widget. It isn't. It's one plain <input> sitting inside a bordered box, plus an array of strings that is the single source of truth. Everything else is a handful of event handlers that all funnel into the same commit function. Here's how I built it, vanilla, no framework, no dependency.

One input, one array

The control is a flex container that wraps: zero or more chips followed by a single text input that flexes to fill the last line. The input is the only real form field; the chips are just a rendering of the array, injected before it. Two CSS tricks sell the illusion of one field — cursor:text plus a mousedown handler focuses the input from anywhere in the padding, and :focus-within draws the ring on the container so tabbing to the inner input lights up the whole box. That array is what a form would post, and it drives everything you see.

let chips = [];   // the single source of truth
Enter fullscreen mode Exit fullscreen mode

Committing text into a chip

A keystroke turns the current text into a chip. The commit keys are Enter and comma (a comma must never end up inside a chip, so we preventDefault). One commit() function is the only funnel — typing, pasting and picking a suggestion all go through it, so the rules live in exactly one place and can't drift apart.

input.addEventListener('keydown', e => {
  if (e.key === 'Enter' || e.key === ','){
    e.preventDefault();          // comma must not land in the text
    commit(input.value);
  }
});
Enter fullscreen mode Exit fullscreen mode

Two ways to remove

Every chip carries a real <button> with a × and an aria-label="Remove {value}"; click it and that index is spliced out. And the classic: pressing Backspace while the input is empty eats the last chip, so a fast typist never has to reach for the mouse. Both paths call the same removeChip(i) — one array operation, then re-render.

Paste splits on commas

People paste alice, bob, carol or a whole column of newline-separated values. I intercept the paste event, and if the text contains a comma or newline I split it and feed every piece through commit(). The same splitter is reused by the Enter/comma path, so typing and pasting behave identically — no special case, no divergence.

const splitParts = raw => raw.split(/[,\n]/);   // comma OR newline
Enter fullscreen mode Exit fullscreen mode

The gatekeepers: dedup, max, validation

Nothing becomes a chip until it passes the gates. Trim it and drop empties. Reject a duplicate (compare case-insensitively) unless duplicates are explicitly allowed. Never exceed the max. Optionally validate the shape — an email regex, say. Crucially, each rejection returns a reason, so the field can shake and speak "that's not a valid email" through a live region instead of silently swallowing input. A rejected value never touches the array; the pipeline is the same every time — split, trim, validate, dedup, cap, render.

Autocomplete and keyboard, done right

Suggestions are a real ARIA combobox: the input gets role="combobox" and aria-expanded, pointing at a role="listbox". The highlighted option is tracked with aria-activedescendant, not real focus — focus stays in the input so typing never breaks. Left-arrow from an empty input steps focus into the chips, where arrow keys roam via roving tabindex and Delete removes, so the keyboard alone can reach any chip. And chip labels are built with textContent, never innerHTML, so a pasted <script> is inert. Reach for a chip input only when values are short, many and equal — tags, recipients, labels. If there's only ever one value, use a plain input; if the set is fixed and small, checkboxes or a multiselect read clearer.

Announce every change

The last piece is the part sighted users never see. An aria-live="polite" status region speaks "Added 3 tags" or the rejection reason, so a screen-reader user is never left guessing why a keystroke did nothing. Paired with the combobox roles and the roving focus, that turns a pile of divs into a control a keyboard-only or non-sighted user can actually operate — the difference between a demo and a field you'd ship.

Type, comma, paste, arrow into the chips — the whole thing runs live here:

https://dev48v.infy.uk/design/day52-chip-input.html

Top comments (1)

Collapse
 
voltradoc profile image
Dr Haina

How many post , you do in a day