Skip to content

Tag

A chip for a filter, a token or a removable item, rendered as <span is="root-tag">.

Usage

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

Unselected Selected
Unselected Selected
<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.

Default Alternate Info Success Warning Danger Neutral
Default Alternate Info Success Warning Danger Neutral
<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:

Default Alternate Info Success Warning Danger Neutral
Default Alternate Info Success Warning Danger Neutral
<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.

Large Medium Small
Large Medium Small
<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.

Hover me Hover me
Hover me Hover me
<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.

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

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

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

Design Design
Design Design
<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".

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

Milano Torino
Milano Torino
<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">.