Pill
A removable token carrying one value the user chose.
import { ... } from '@corndev/ui' HTML
<span class="corn-pill" data-emphasis="soft" data-size="md">
<span class="corn-pill-label">production</span>
<button type="button" class="corn-pill-remove" aria-label="Remove production"></button>
</span>
Classes
| Class |
|---|
.corn-pill |
.corn-pill-label |
.corn-pill-remove |
Attributes
| Attribute | Values |
|---|---|
data-size | sm|md|lg |
data-emphasis | soft|solid|outline|quiet |
data-disabled | present or absent |
Vanilla JS
Required for behaviour — adds names the remove control from the pill’s own text and reports the value when it is activated.
import { createPill } from '@corndev/ui';
import '@corndev/ui/theme';
const el = document.querySelector('#target');
const instance = createPill(el, { /* options */ });
Options
| Option | Type | Default | Description |
|---|---|---|---|
emphasis | 'solid'|'soft'|'outline'|'quiet' | 'soft' | How present the token looks — the same axis and the same names as Button and Badge, on the one colour a pill has. A pill is always neutral: it holds a value someone chose rather than a status, so there is no colour to cross this with. outline and quiet are the unselected-looking rungs. |
size | 'sm'|'md'|'lg' | 'md' | Type scale and horizontal padding. The height is fixed by the remove control’s target minimum and does not change. |
disabled | boolean | false | Dims the pill and disables its remove control. This is the native disabled attribute on the button, so the control really is unreachable rather than only looking that way. |
removeLabel | string | undefined | Accessible name for the remove control, overriding the generated Remove <value>. Give one when the pill’s text is not the thing being removed. |
onRemove | (value: string) => void | undefined | Called when the remove control is activated, with the pill’s text. Nothing is removed from the DOM — what a removed value means belongs to the thing holding the list. |
React
Pill — from @corndev/ui-react.
The remove control is rendered only when onRemove is given — a pill that cannot be removed has no button, rather than a disabled one, since a disabled button is still announced. onRemove receives the pill’s text, or value when the children are not plain text. Nothing is removed from the DOM: what a removed value means belongs to whatever holds the list.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | undefined | The value shown in the pill. |
emphasis | 'solid'|'soft'|'outline'|'quiet' | 'soft' | How present the token looks — the same axis and the same names as Button and Badge, on the one colour a pill has. A pill is always neutral: it holds a value someone chose rather than a status, so there is no colour to cross this with. outline and quiet are the unselected-looking rungs. |
size | 'sm'|'md'|'lg' | 'md' | Type scale and horizontal padding. The height is fixed by the remove control’s target minimum. |
disabled | boolean | false | Dims the pill and disables its remove control with the native attribute. |
onRemove | (value: string) => void | undefined | Called with the pill’s value when the remove control is activated. Omit it for a pill that cannot be removed — the control is not rendered at all rather than rendered inert. |
removeLabel | string | undefined | Accessible name for the remove control, overriding the generated Remove <value>. Give one when children is not plain text. |
value | string | undefined | The value passed to onRemove and used in the generated remove label. Defaults to children when it is a string. |
className | string | undefined | Merged with the Corn UI classes. |
Pill or Badge?
Both are small and rounded, and they are not interchangeable.
A badge says something about a thing — a status, a count, a category. It is
a <span> and nobody clicks it.
A pill is the thing. It holds a value the user chose and carries the control that takes it back.
<Badge variant="success">Published</Badge>
<Pill onRemove={() => {}}>production</Pill>That difference decides the size, too. WCAG 2.5.8 asks for a 24px pointer
target, and a badge is 15px, 18px or 22px tall depending on its size — none of
them can hold a conforming remove control. So a pill is 1.5rem tall at every
size, and only the type and the horizontal padding change. If you want a label
that gets smaller, you want a badge.
Emphasis
The same axis and the same four names as Button and Badge, on the one colour a
pill has. A pill is always neutral — it holds a value someone chose, not a
status — so there is no colour to cross this with. outline and quiet are the
unselected-looking rungs.
<Pill emphasis="outline" onRemove={drop}>production</Pill>soft is the default and the look a pill has always had, so nothing changes
unless you ask. No rung touches the box, so the remove control keeps its 24px
target at every one.
Removable
The remove control is only rendered when you pass onRemove. A pill that cannot
be removed has no button at all rather than a disabled one, because a disabled
button is still announced and there is nothing there to announce.
onRemove receives the pill’s text. Nothing is removed from the DOM — what a
removed value means belongs to whatever holds the list.
<Pill onRemove={value => setTags(tags.filter(t => t !== value))}>
production
</Pill>
<Pill>read only</Pill>Naming the remove control
The button is named for the value it removes — Remove production, not
Remove. This is the part that breaks silently: six pills whose buttons are all
called “Remove” are perfectly usable by sight and useless by ear.
You get the generated name for free when the children are plain text. When they
are not, give value so there is something to name it with, or removeLabel to
write the name yourself.
<Pill onRemove={remove} value="production">
<strong>production</strong>
</Pill>
<Pill onRemove={remove} removeLabel="Stop deploying to staging">
staging
</Pill>Sizes
Only the type and the horizontal padding change. The height is the target minimum and stays put.
<Pill size="sm" onRemove={remove}>Small</Pill>
<Pill size="md" onRemove={remove}>Medium</Pill>
<Pill size="lg" onRemove={remove}>Large</Pill>Long values
A pill holds something someone typed, so it can be any length. The label truncates rather than pushing the rest of a row out of view, and the full text stays in the remove control’s accessible name.
<div style="max-width: 200px">
<Pill onRemove={remove}>an-extremely-long-tag-name-that-will-not-fit</Pill>
</div>