Loading…
Loading…
A compact element used for categorization, filtering, or displaying metadata.
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:
When NOT to use a Tag:
Explore semantic tag colors with the Color Tool and verify tag text meets contrast thresholds with the Contrast Checker.
| 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. |
| 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 | 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 |
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 |
| 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 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.
| 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. |
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:
<button aria-pressed="true|false">. Screen reader announces "JavaScript, toggle button, pressed." This is the simplest pattern for filter chips.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.
Do:
Don't:
Tag input best practices:
<!-- 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>
);
}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.