Pill

A removable token carrying one value the user chose.

import { ... } from '@corndev/ui'
productionstaginga-very-long-environment-name
SmallMediumLargeNot removableDisabled

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

AttributeValues
data-sizesm|md|lg
data-emphasissoft|solid|outline|quiet
data-disabledpresent 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

OptionTypeDefaultDescription
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.
disabledbooleanfalseDims 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.
removeLabelstringundefinedAccessible 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) => voidundefinedCalled 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

PropTypeDefaultDescription
childrenReact.ReactNodeundefinedThe 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.
disabledbooleanfalseDims the pill and disables its remove control with the native attribute.
onRemove(value: string) => voidundefinedCalled 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.
removeLabelstringundefinedAccessible name for the remove control, overriding the generated Remove <value>. Give one when children is not plain text.
valuestringundefinedThe value passed to onRemove and used in the generated remove label. Defaults to children when it is a string.
classNamestringundefinedMerged 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.

Publishedproduction
<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.

solidsoftoutlinequiet
<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.

productionread only
<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.

productionstaging
<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.

SmallMediumLarge
<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.

an-extremely-long-tag-name-that-will-not-fit
<div style="max-width: 200px">
  <Pill onRemove={remove}>an-extremely-long-tag-name-that-will-not-fit</Pill>
</div>