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
| Variant | Purpose | Visual Treatment |
|---|---|---|
| Solid | High-emphasis categorization. | Filled background with contrasting text. |
| Subtle / Tonal | Medium-emphasis, the most common default. | Tinted background (10–15% opacity) with saturated text. |
| Outline | Low-emphasis, lightweight metadata. | Transparent background, colored border. |
| Selected | Active filter chip or toggled state. | Solid or heavy fill indicating active selection. Checkmark icon optional. |
Interaction Variants
| Type | Behavior | Visual Cues |
|---|---|---|
| Static | No interaction. Read-only categorization label. | No hover/click effects. Cursor default. |
| Clickable | Clicking triggers an action (navigates to filtered view, selects/deselects). | Hover background change, cursor pointer, focus ring. |
| Removable | Includes a close (×) button to remove the tag. | Trailing × icon. Clicking × removes; clicking body may still be an action. |
| Selectable | Toggles between selected/unselected states. | Visual change on selection (fill changes, checkmark appears). |
| Input tag | Created by the user in a tag input field. | Typically removable. Appears inline within a text input container. |
Size Variants
| Size | Height | Padding | Font Size | Use Case |
|---|---|---|---|---|
| Small (sm) | 22px | 4px 8px | 11px | Dense tables, inline metadata |
| Medium (md) | 28px | 4px 10px | 13px | Default for most contexts |
| Large (lg) | 34px | 6px 14px | 14px | Filter 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:
| Color | Example Use |
|---|---|
| Gray | Default, uncategorized |
| Blue | Type: "Feature", "Enhancement" |
| Green | Status: "Approved", "Resolved" |
| Yellow | Priority: "Medium", "Review needed" |
| Red | Priority: "Critical", "Bug" |
| Purple | Type: "Idea", "Research" |
| Custom | User-defined colors for personal labels |
Properties
Tag Properties
| Property | Type | Default | Description |
|---|---|---|---|
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 |
removable | boolean | false | Shows a remove (×) button |
onRemove | () => void | — | Callback when remove button is clicked |
selected | boolean | false | Whether the tag is in selected state |
onClick | () => void | — | Click handler (makes tag interactive) |
disabled | boolean | false | Prevents all interaction |
leftIcon | ReactNode | — | Icon before the label |
avatar | ReactNode | — | Avatar element before the label (for people tags) |
maxWidth | number | string | — | Truncates label with ellipsis beyond this width |
children | ReactNode | — | 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 Category | Token Example | Tag Usage |
|---|---|---|
| Color – Solid Fill | --color-blue-600 | Solid variant background |
| Color – Subtle Fill | --color-blue-100 | Subtle variant tinted background |
| Color – Subtle Text | --color-blue-700 | Subtle variant text color |
| Color – Outline Border | --color-blue-300 | Outline variant border |
| Color – On Fill | --color-white | Solid variant text |
| Color – Selected Fill | --color-blue-600 | Selected state background |
| Border Radius | --radius-md (6px) or --radius-full | Tag corner rounding |
| Typography | --font-size-xs / --font-size-sm, --font-weight-medium | Tag 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 Size | 12px / 14px | Remove icon and left icon |
| Transition | --duration-fast | Hover, selection, removal animations |
| Color – Remove Hover | --color-blue-200 | Remove 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
| State | Visual Change | Notes |
|---|---|---|
| Default | Standard appearance per variant and color. | — |
| Hover (interactive) | Slight background darkening or elevation. | Only for clickable or selectable tags. |
| Focused | Visible focus ring. | Keyboard focus. Must meet 3:1 contrast (WCAG 1.4.11). |
| Selected | Fill changes to solid, optional checkmark icon appears. | aria-pressed="true" (toggle) or aria-selected="true" (list context). |
| Disabled | Muted colors, no hover/click. | aria-disabled="true". |
| Removing | Fade-out or scale-down animation, then removal from DOM. | Announce removal to screen readers via live region: "Tag 'JavaScript' removed." |
| Truncated | Label 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:
- Toggle buttons: Render as
<button aria-pressed="true|false">. Screen reader announces "JavaScript, toggle button, pressed." This is the simplest pattern for filter chips. - Listbox options: When tags form a selection group, wrap in
role="listbox"with each tag asrole="option"andaria-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
<!-- 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>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.