Skip to content

Tag

A compact element used for categorization, filtering, or displaying metadata.

  • Chip
  • Label
  • Token

Overview

The Tag (also called Chip, Token, or Label) is a compact, interactive element used for categorization, filtering, and user-generated metadata. Unlike a Badge which is a passive status indicator controlled by the system, a tag is typically user-facing and often interactive — users add, remove, and select tags as part of their workflow.

Tags appear in two fundamental contexts: display tags (read-only labels showing categories or attributes) and input tags (user-created tokens in a tag input or filter bar). Both forms share visual language but differ in interaction — display tags may be selectable for filtering, while input tags are removable and creatable.

When to use a Tag:

  • To display categorical metadata (topic, genre, language, technology)
  • To represent user-applied labels or filters (email labels, task tags)
  • In a tag input field where users type and create tokens
  • As selectable filter chips (e.g., "All | Unread | Starred")
  • To show removable selections (multi-select chips showing chosen values)

When NOT to use a Tag:

  • For system-generated status indicators — use a Badge
  • For notification counts — use a Badge with count
  • For primary actions — use a Button
  • For navigation — use Tabs or a Navigation Bar

Explore semantic tag colors with the Color Tool and verify tag text meets contrast thresholds with the Contrast Checker.

Variants

Tag Variants

VariantPurposeVisual Treatment
SolidHigh-emphasis categorization.Filled background with contrasting text.
Subtle / TonalMedium-emphasis, the most common default.Tinted background (10–15% opacity) with saturated text.
OutlineLow-emphasis, lightweight metadata.Transparent background, colored border.
SelectedActive filter chip or toggled state.Solid or heavy fill indicating active selection. Checkmark icon optional.

Interaction Variants

TypeBehaviorVisual Cues
StaticNo interaction. Read-only categorization label.No hover/click effects. Cursor default.
ClickableClicking triggers an action (navigates to filtered view, selects/deselects).Hover background change, cursor pointer, focus ring.
RemovableIncludes a close (×) button to remove the tag.Trailing × icon. Clicking × removes; clicking body may still be an action.
SelectableToggles between selected/unselected states.Visual change on selection (fill changes, checkmark appears).
Input tagCreated by the user in a tag input field.Typically removable. Appears inline within a text input container.

Size Variants

SizeHeightPaddingFont SizeUse Case
Small (sm)22px4px 8px11pxDense tables, inline metadata
Medium (md)28px4px 10px13pxDefault for most contexts
Large (lg)34px6px 14px14pxFilter bars, prominent categorization

Color Variants

Tags use the same semantic color palette as badges but with more emphasis on user-assigned meaning. Use the Color Tool to generate consistent color systems:

ColorExample Use
GrayDefault, uncategorized
BlueType: "Feature", "Enhancement"
GreenStatus: "Approved", "Resolved"
YellowPriority: "Medium", "Review needed"
RedPriority: "Critical", "Bug"
PurpleType: "Idea", "Research"
CustomUser-defined colors for personal labels

Properties

Tag Properties

PropertyTypeDefaultDescription
variant'solid' | 'subtle' | 'outline''subtle'Visual style
color'gray' | 'blue' | 'green' | 'yellow' | 'red' | 'purple' | string'gray'Color scheme. String allows custom hex.
size'sm' | 'md' | 'lg''md'Controls height, padding, font size
removablebooleanfalseShows a remove (×) button
onRemove() => void—Callback when remove button is clicked
selectedbooleanfalseWhether the tag is in selected state
onClick() => void—Click handler (makes tag interactive)
disabledbooleanfalsePrevents all interaction
leftIconReactNode—Icon before the label
avatarReactNode—Avatar element before the label (for people tags)
maxWidthnumber | string—Truncates label with ellipsis beyond this width
childrenReactNode—Tag label content

Important: When a tag is both clickable and removable, clicking the body and clicking the × must be distinct targets. The × button should have its own aria-label (e.g., "Remove tag: JavaScript") separate from the tag's clickable action.

Token Mappings

Design Token Mappings

Token CategoryToken ExampleTag Usage
Color – Solid Fill--color-blue-600Solid variant background
Color – Subtle Fill--color-blue-100Subtle variant tinted background
Color – Subtle Text--color-blue-700Subtle variant text color
Color – Outline Border--color-blue-300Outline variant border
Color – On Fill--color-whiteSolid variant text
Color – Selected Fill--color-blue-600Selected state background
Border Radius--radius-md (6px) or --radius-fullTag corner rounding
Typography--font-size-xs / --font-size-sm, --font-weight-mediumTag label
Spacing – Padding--space-1 (4px) / --space-2.5 (10px)Vertical / horizontal padding
Spacing – Gap--space-1 (4px)Gap between icon/avatar and label, between label and remove button
Icon Size12px / 14pxRemove icon and left icon
Transition--duration-fastHover, selection, removal animations
Color – Remove Hover--color-blue-200Remove button hover background (circle)

Use the Color Tool to generate the full subtle/solid/outline token set from a single base hue.

States

Tag States

StateVisual ChangeNotes
DefaultStandard appearance per variant and color.—
Hover (interactive)Slight background darkening or elevation.Only for clickable or selectable tags.
FocusedVisible focus ring.Keyboard focus. Must meet 3:1 contrast (WCAG 1.4.11).
SelectedFill changes to solid, optional checkmark icon appears.aria-pressed="true" (toggle) or aria-selected="true" (list context).
DisabledMuted colors, no hover/click.aria-disabled="true".
RemovingFade-out or scale-down animation, then removal from DOM.Announce removal to screen readers via live region: "Tag 'JavaScript' removed."
TruncatedLabel truncated with ellipsis when exceeding maxWidth.Full label shown on hover via Tooltip.
Remove button hover× icon gets a circular background highlight.Distinct hover target from the tag body.

Accessibility

Accessibility

Tags combine multiple ARIA patterns depending on their interaction mode. Getting the roles right is essential.

Static tags: Use <span> with no role. They're inline text elements. Screen readers will read the tag text naturally. Ensure contrast meets 4.5:1 (WCAG 1.4.3) — verify with the Contrast Checker.

Clickable / selectable tags: Two approaches:

  1. Toggle buttons: Render as <button aria-pressed="true|false">. Screen reader announces "JavaScript, toggle button, pressed." This is the simplest pattern for filter chips.
  2. Listbox options: When tags form a selection group, wrap in role="listbox" with each tag as role="option" and aria-selected. Better for multi-select filter groups.

Removable tags: The remove button must be a separate focusable element within the tag. Use <button aria-label="Remove tag: JavaScript"> for the × icon. The tag itself should not be a button if it has no click action beyond removal — use a <span> with the remove <button> inside.

WCAG 1.4.1 — Use of Color: Tags that use color to convey category must also include text labels. A red tag labeled "Bug" is accessible. A red tag with no text, relying purely on color to mean "critical," is not. Use the Color Tool to pair colors with appropriate contrast values.

WCAG 2.1.1 — Keyboard: All interactive tags must be keyboard operable. Clickable tags: Enter/Space activates. Removable tags: Tab to the remove button, Enter/Space to remove. In a tag group, use arrow key navigation (roving tabindex) to reduce tab stops.

WCAG 4.1.2 — Name, Role, Value: Each interactive tag needs a clear accessible name. For icon-only remove buttons, aria-label is required. For tags in a group, the group should have aria-label describing its purpose ("Filter by technology").

WCAG 1.4.11 — Non-Text Contrast: The tag boundary (outline or fill) must achieve 3:1 contrast against the surrounding background. Subtle tags on white backgrounds often fail this — test carefully.

Live region announcements: When tags are added or removed dynamically (in a tag input), announce changes: "Added tag: React" / "Removed tag: Vue." Use aria-live="polite" on a status container.

Usage Guidelines

Usage Guidelines

Do:

  • Keep tag labels short (1–3 words); truncate with ellipsis and a tooltip if necessary
  • Use consistent colors for the same categories across your application
  • Provide clear visual distinction between selected and unselected tags
  • Animate tag addition/removal for spatial continuity (fade, scale)
  • Group related tags and label the group (e.g., "Technologies", "Priority")

Don't:

  • Don't use tags as mini-buttons for primary actions
  • Don't create more than 6–8 color categories — beyond that, colors become indistinguishable
  • Don't use tags and badges interchangeably — they serve different purposes (user metadata vs. system status)
  • Don't force users to tag content with no undo — always allow removal
  • Don't wrap tags to more than 3 lines in a tag group — use a "+N more" overflow with a popover

Tag input best practices:

  • Allow free-text creation (typing + Enter creates a new tag)
  • Provide autocomplete suggestions from existing tags
  • Validate for duplicates (prevent the same tag twice)
  • Set a maximum tag count when appropriate (e.g., "Add up to 5 tags")
  • Allow both keyboard (Backspace to delete last tag) and mouse (click ×) removal

Code Snippets

html
<!-- Subtle tag -->
<span class="tag tag--subtle tag--blue">JavaScript</span>

<!-- Removable tag -->
<span class="tag tag--subtle tag--green">
  <span class="tag__label">React</span>
  <button class="tag__remove" aria-label="Remove tag: React">
    <svg aria-hidden="true" width="12" height="12"><!-- × icon --></svg>
  </button>
</span>

<!-- Selectable tag (toggle button) -->
<button
  class="tag tag--outline tag--purple"
  aria-pressed="false"
  onclick="this.setAttribute('aria-pressed', this.getAttribute('aria-pressed') === 'true' ? 'false' : 'true')"
>
  <span class="tag__label">Design</span>
</button>

<!-- Tag group (filter chips) -->
<div role="group" aria-label="Filter by category" class="tag-group">
  <button class="tag tag--solid tag--blue" aria-pressed="true">
    <svg aria-hidden="true"><!-- check icon --></svg>
    <span class="tag__label">All</span>
  </button>
  <button class="tag tag--outline tag--gray" aria-pressed="false">
    <span class="tag__label">Articles</span>
  </button>
  <button class="tag tag--outline tag--gray" aria-pressed="false">
    <span class="tag__label">Videos</span>
  </button>
</div>

<style>
.tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
  line-height: 1;
  border-radius: var(--radius-md);
  padding: 4px 10px;
  border: none;
  cursor: default;
  white-space: nowrap;
  transition: background var(--duration-fast), color var(--duration-fast);
}
.tag--subtle.tag--blue {
  background: var(--color-blue-100);
  color: var(--color-blue-700);
}
.tag--outline.tag--gray {
  background: transparent;
  border: 1px solid var(--color-gray-300);
  color: var(--color-gray-700);
}
button.tag {
  cursor: pointer;
}
button.tag:hover {
  filter: brightness(0.95);
}
button.tag[aria-pressed="true"] {
  background: var(--color-blue-600);
  color: white;
}
.tag__remove {
  all: unset;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  cursor: pointer;
}
.tag__remove:hover {
  background: rgba(0, 0, 0, 0.1);
}
.tag-group {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
</style>
tsx
interface TagProps {
  variant?: 'solid' | 'subtle' | 'outline';
  color?: 'gray' | 'blue' | 'green' | 'yellow' | 'red' | 'purple';
  size?: 'sm' | 'md' | 'lg';
  removable?: boolean;
  onRemove?: () => void;
  selected?: boolean;
  onClick?: () => void;
  disabled?: boolean;
  leftIcon?: React.ReactNode;
  maxWidth?: number | string;
  children: React.ReactNode;
}

function Tag({
  variant = 'subtle',
  color = 'gray',
  size = 'md',
  removable = false,
  onRemove,
  selected = false,
  onClick,
  disabled = false,
  leftIcon,
  maxWidth,
  children,
}: TagProps) {
  const isInteractive = !!onClick;
  const Component = isInteractive ? 'button' : 'span';

  return (
    <Component
      className={[
        'tag',
        `tag--${variant}`,
        `tag--${color}`,
        `tag--${size}`,
        selected && 'tag--selected',
        disabled && 'tag--disabled',
      ].filter(Boolean).join(' ')}
      onClick={!disabled ? onClick : undefined}
      disabled={isInteractive ? disabled : undefined}
      aria-pressed={isInteractive ? selected : undefined}
      aria-disabled={!isInteractive && disabled ? true : undefined}
      style={{ maxWidth }}
    >
      {selected && (
        <svg className="tag__check" aria-hidden="true" width="12" height="12" viewBox="0 0 12 12">
          <path d="M10 3L4.5 8.5L2 6" stroke="currentColor" strokeWidth="2" fill="none" />
        </svg>
      )}
      {leftIcon && <span className="tag__icon">{leftIcon}</span>}
      <span className="tag__label" style={maxWidth ? {
        overflow: 'hidden',
        textOverflow: 'ellipsis',
        whiteSpace: 'nowrap',
      } : undefined}>
        {children}
      </span>
      {removable && (
        <button
          className="tag__remove"
          onClick={(e) => {
            e.stopPropagation();
            onRemove?.();
          }}
          aria-label={`Remove tag: ${typeof children === 'string' ? children : ''}`}
          disabled={disabled}
          tabIndex={0}
        >
          <svg aria-hidden="true" width="12" height="12" viewBox="0 0 12 12">
            <path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" strokeWidth="1.5" />
          </svg>
        </button>
      )}
    </Component>
  );
}

// Tag input component
interface TagInputProps {
  value: string[];
  onChange: (tags: string[]) => void;
  placeholder?: string;
  maxTags?: number;
}

function TagInput({ value, onChange, placeholder = 'Add tag...', maxTags }: TagInputProps) {
  const [input, setInput] = React.useState('');
  const inputRef = React.useRef<HTMLInputElement>(null);

  const addTag = (tag: string) => {
    const trimmed = tag.trim();
    if (!trimmed || value.includes(trimmed)) return;
    if (maxTags && value.length >= maxTags) return;
    onChange([...value, trimmed]);
    setInput('');
  };

  return (
    <div className="tag-input" onClick={() => inputRef.current?.focus()}>
      {value.map((tag) => (
        <Tag key={tag} size="sm" removable onRemove={() => onChange(value.filter((t) => t !== tag))}>
          {tag}
        </Tag>
      ))}
      <input
        ref={inputRef}
        className="tag-input__field"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyDown={(e) => {
          if (e.key === 'Enter' || e.key === ',') {
            e.preventDefault();
            addTag(input);
          }
          if (e.key === 'Backspace' && !input && value.length) {
            onChange(value.slice(0, -1));
          }
        }}
        placeholder={!maxTags || value.length < maxTags ? placeholder : ''}
        disabled={maxTags ? value.length >= maxTags : false}
        aria-label="Add tag"
      />
    </div>
  );
}

Design Systems

Design System Implementations

Material Design 3 calls this component "Chip." MUI provides Chip with variant="filled|outlined", color, label, avatar, icon, deleteIcon, onDelete (enables the × button), clickable, and size="small|medium". Material distinguishes four chip types: Assist (action), Filter (toggle selection with checkmark), Input (user-entered, removable), and Suggestion (dynamically generated). This categorization is the most nuanced in any major design system and is worth studying. Filter chips use selected prop and show a leading checkmark when active.

Ant Design provides Tag with closable, onClose, color (preset string or custom hex), and icon. Ant's Tag.CheckableTag is a separate component for selectable/toggle tags with checked and onChange. Ant allows custom close icons and supports both controlled and uncontrolled close behavior. The color prop accepts preset names ("magenta", "red", "volcano", etc.) or any hex value — flexible but potentially inconsistent.

Chakra UI provides Tag with size, variant="subtle|solid|outline", colorScheme, and sub-components TagLabel, TagLeftIcon, TagRightIcon, TagCloseButton. Chakra's composition model makes it easy to construct complex tags: <Tag><Avatar size="xs" /><TagLabel>Niko</TagLabel><TagCloseButton /></Tag>.

Radix UI does not provide a Tag/Chip primitive. Tags are considered application-level styled elements. For the toggle behavior, Radix's ToggleGroup provides the selection semantics that filter chips need.

Headless UI does not include a tag component.

shadcn/ui offers a Badge component (which functions as both badge and tag in its API) with variant="default|secondary|destructive|outline". For removable/interactive behavior, developers extend it manually.

Use the Color Tool to build a consistent tag color palette that works in both light and dark modes.

Data DisplayChipLabelToken