Tag
A chip for a filter, a token or a removable item, rendered as
<span is="root-tag">.
Usage
<span is="root-tag" data-color="default">Design</span>
data-variant
and data-color work the way they do on Button: the variant decides which properties take the
color, the palette supplies it. A tag with neither is the filled Default palette - a chip is never
colorless.
The three heights are the Badge's, on a radius two bands lower: a tag stays rounded without becoming a pill, which is the Badge's shape. A tag with no label is a Badge, so the label is not optional.
Variants (TODO: check with design)
primary
fills with the color - Figma calls that one Fill - and secondary keeps it on the
border and the label over the page ground, Figma's Outline. The names are Button's, so one
vocabulary covers the library. In a group of filters the two read as selected and not selected, which is
why there is no selected state to write: the variant carries it.
That reading is not settled - whether a filled tag is "selected" and an
outlined one "not selected" is the open question with design, which is what the TODO on this
heading is for.
<span is="root-tag" data-variant="secondary" data-color="default">Unselected</span>
<span is="root-tag" data-variant="primary" data-color="default">Selected</span>
Palettes
data-color
names a ramp: default, alternate, info, success,
warning,
danger
and neutral. At rest the tag reads that ramp's -400 step; a filled label is one
of the palette's two poles - --root-c--light or --root-c--dark, the page's own
text pair - picked per step by measured contrast, which is how the same seven names keep working in the
dark scheme, where those steps sit at the other end of their ramps.
<span is="root-tag" data-color="default">Default</span>
<span is="root-tag" data-color="alternate">Alternate</span>
<span is="root-tag" data-color="info">Info</span>
<span is="root-tag" data-color="success">Success</span>
<span is="root-tag" data-color="warning">Warning</span>
<span is="root-tag" data-color="danger">Danger</span>
<span is="root-tag" data-color="neutral">Neutral</span>
Every palette takes secondary:
<span is="root-tag" data-variant="secondary" data-color="default">Default</span>
<span is="root-tag" data-variant="secondary" data-color="alternate">Alternate</span>
<span is="root-tag" data-variant="secondary" data-color="info">Info</span>
<span is="root-tag" data-variant="secondary" data-color="success">Success</span>
<span is="root-tag" data-variant="secondary" data-color="warning">Warning</span>
<span is="root-tag" data-variant="secondary" data-color="danger">Danger</span>
<span is="root-tag" data-variant="secondary" data-color="neutral">Neutral</span>
Sizes
data-size
takes small, medium and large; medium is the default and needs no
value written. Measured heights are 31.25px for large and 25px for both medium and small - the two share a
radius, so they share a height, and only the type changes between them (16 / 14.2 / 12.6px). Large also
takes the next radius step up.
<span is="root-tag" data-color="default" data-size="large">Large</span>
<span is="root-tag" data-color="default">Medium</span>
<span is="root-tag" data-color="default" data-size="small">Small</span>
States
Hover moves the color one ramp step deeper, :active two - -400,
-500, -600 - and the variant decides what follows: the surface in
primary, the border and the label in secondary. A filled label moves with it
where the step demands the other pole: Danger flips light on hover, Neutral while pressed, which is what
Figma draws.
<span is="root-tag" data-color="success">Hover me</span>
<span is="root-tag" data-variant="secondary" data-color="success">Hover me</span>
aria-disabled="true"
steps out of the palette entirely: Neutral 200 where the color was, Neutral 600 on the label, in both
variants, and the (x) stops taking the pointer. aria-disabled rather than
disabled, which a <span> does not have.
<span is="root-tag" data-color="default" aria-disabled="true">Disabled</span>
<span is="root-tag" data-variant="secondary" data-color="default" aria-disabled="true">Disabled</span>
Removable
data-removable
reads right, left or none, and the component writes
right when the markup does not: a tag carries the (x) unless it is told not to, and the value
in the DOM says which of the three it is. An unknown value falls back to right, because a
typo should not silently delete the control.
The (x) itself is one round button the size of the type, in the tag's own text color, held at 70% until
it is hovered or focused. It is the component's - the markup does not author it, and a tag whose markup
already carries a [data-part="remove"] button adopts that one instead of adding a
second.
data-removable="left"
moves it to the other side. Put a custom icon on the side the (x) is not: two symbols side by side
cannot be told apart, which is the design's own constraint.
<span is="root-tag" data-color="default" data-removable="right">Milano</span>
<span is="root-tag" data-color="default" data-removable="left">Milano</span>
The tab order goes through the tag first and its (x) second, which is the design's own order: the
component sets tabindex="0" on the tag and the button is a button. No interactive
role is set on the tag - role="button" or
role="checkbox" would make its children presentational and take the (x) out of
the accessibility tree - so what activating the tag means is the consumer's to wire; the design
has no Selected state because the variant carries selection. The ring on both is the foundations',
tinted by the palette.
The (x) fires root-tag.remove before detaching the tag. data-value is what the
event carries when the label is not the value.
<span is="root-tag" data-color="success" data-value="published" data-removable="right">
<i is="root-icon" data-icon="check"></i>Published
</span>
Cases
The ones the design calls out, in its own order.
A filter that is not selected is secondary: the shape communicates state
better than the color, and it works for people who cannot tell the colors apart. A
selected
filter is primary in the same palette, not a different one.
<span is="root-tag" data-variant="secondary" data-color="default">Design</span>
<span is="root-tag" data-variant="primary" data-color="default">Design</span>
A custom icon plus the (x), one per side: the icon is swappable, the (x) is not - an (o) does not read as "remove".
<span is="root-tag" data-color="success" data-removable="right">
<i is="root-icon" data-icon="check"></i>Published
</span>
Inside a field, which is why the tag is not button-height: at 25px it fits inside a 36-40px field, border included, and the 4px of border is part of that height in both variants. Under a text input, the tags are what has been entered so far - each one removable, and each one carrying the value the field will submit.
<input is="root-input" type="text" data-label="Cities" data-icon-start="funnel" placeholder="Add a city" />
<span data-part="tags" role="group" aria-label="Cities added">
<span is="root-tag" data-color="neutral" data-size="small" data-removable="right">Milano</span>
<span is="root-tag" data-color="neutral" data-size="small" data-removable="right">Torino</span>
</span>
One wrapper and not a <ul> of one-tag <li>s: the tags under a
field are a group of values, not an ordered reading, and the list markup bought nothing the component
needed - Tag styles and behaves the same wherever it is put. The wrapper keeps a name, so a reader still
hears what the group is before hearing what is in it.
A multi-tag select
is the case that height is really for: the reader picks one or more options from the select's own
dropdown and each pick becomes a tag inside the field, removable there. That composite does not
exist yet - a <select multiple> takes
<option>
children and nothing else, so the tags have to be placed in the
[field-wrapper]
by the component that owns the field, not authored. What Tag brings to it is the 25px height that fits a
36-40px field with its border inside, data-value for the option each tag stands for, and
root-tag.remove for the field to listen to.
Tracked on the roadmap under what comes next. No demo here for it: a row of tags parked next to a select is not the case being described - it is two controls that happen to sit together - and a demo that misrepresents what it documents is worse than none.
Events
Namespaced as <component>.<event>, bubbling and cancelable.
| Event | Detail | Description |
|---|---|---|
root-tag.remove |
{ value } |
Fired when the (x) is activated, before the tag detaches |
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-variant |
'primary' | 'secondary' |
'primary' |
Fill, or the color on the border and the label |
data-color |
'default' | 'alternate' | 'info' | 'success' | 'warning' | 'danger' | 'neutral'
|
'default' |
Which ramp supplies the color |
data-size |
'small' | 'medium' | 'large' |
'medium' |
Height, radius and type step |
data-removable |
'right' | 'left' | 'none' |
'right' |
Which side the (x) takes, or no (x) at all; written by the component when absent |
data-value |
string |
the label | What root-tag.remove carries |
aria-disabled |
'true' |
- | Out of the palette, and the (x) stops responding |
tabindex |
string |
'0' |
Set by the component when absent, so the tag is a focus target |
aria-label |
string |
- | Accessible name, when the label alone is not one |
Extends
HTMLSpanElement - use with <span is="root-tag">.