PillsInput
Pills laid out beside a text input, drawn as one field.
import { ... } from '@corndev/ui' Enter adds a tag. Backspace in an empty field removes the last one.
HTML
<div class="corn-pills-input" data-size="md">
<span class="corn-pill" data-size="md">
<span class="corn-pill-label">production</span>
<button type="button" class="corn-pill-remove" aria-label="Remove production"></button>
</span>
<input class="corn-pills-input-field" aria-label="Add a tag" />
</div>
Classes
| Class |
|---|
.corn-pills-input |
.corn-pills-input-field |
Attributes
| Attribute | Values |
|---|---|
data-size | sm|md|lg |
data-disabled | present or absent |
data-error | present or absent |
Vanilla JS
Required for behaviour — adds focuses the input when the field’s empty space is clicked.
import { createPillsInput } from '@corndev/ui';
import '@corndev/ui/theme';
const el = document.querySelector('#target');
const instance = createPillsInput(el, { /* options */ });
Options
| Option | Type | Default | Description |
|---|---|---|---|
size | 'sm'|'md'|'lg' | 'md' | Padding and type scale. The pills inside inherit it. |
disabled | boolean | false | Dims the field and disables the inner input with the native attribute. |
error | boolean | false | Paints the error border and focus ring. The message itself belongs to the field around this, not here. |
React
PillsInput, PillsInputField — from @corndev/ui-react.
Takes the pills and the field as children rather than a values array, so the thing holding the list keeps deciding what a value is and what removing one means. PillsInputField is a plain <input> with the chrome removed — value, onChange, onKeyDown and the rest go straight through it.
The props are the same as the options above, plus className.
What it is and is not
PillsInput is the field: the border, the one focus ring, and the layout
that keeps pills and a text input looking like a single control.
It is not a value editor. It takes the pills and the input as children rather
than a values array, so whatever holds the list keeps deciding what a value is
and what removing one means. MultiSelect and TagsInput are the components
that own that decision; this is what they are drawn in. Use it directly when you
want that control yourself.
<PillsInput>
{tags.map(tag => (
<Pill key={tag} onRemove={value => setTags(tags.filter(t => t !== value))}>
{tag}
</Pill>
))}
<PillsInputField
aria-label="Add a tag"
value={draft}
onChange={event => setDraft(event.target.value)}
/>
</PillsInput>One focus ring
The ring is on the wrapper, from :focus-within, and the inner input has no
chrome of its own. Two borders nested inside one another is the thing this
component exists to avoid.
:focus-within catches the remove buttons as well as the input, which is
right — a keyboard user tabbing onto a pill’s remove control is still inside
this field.
Why the wrapper is a div
Worth knowing before you simplify it. A <label> wrapper would give
click-anywhere-to-focus with no JavaScript, and it is a trap: a label with no
for targets the first labelable descendant in tree order, and <button>
is labelable. With pills before the input, clicking the field’s empty space
fires the first pill’s remove control and deletes a value. Measured in Chromium,
from 25px outside the button’s own box.
So click-to-focus is the one thing here that needs JavaScript. Everything else — the border, the ring, the wrapping layout — is the stylesheet, and the markup works without a script.
States
<PillsInput error>
<Pill onRemove={remove}>invalid</Pill>
<PillsInputField aria-label="A field in error" />
</PillsInput>error paints the border and ring only. The message itself belongs to the
Field around this, which owns the label,
the description and the aria-describedby wiring.
disabled dims the field; pass it to the inner control too, since the control
is a child here and nothing is reaching in to disable it for you.
Naming the input
The input needs its own accessible name — aria-label, or a Field around the
whole thing. The pills beside it are values, not a label, and a screen reader
reading “openapi graphql” as the name of a text box helps nobody.