可交互标签组件
一个支持添加、删除、编辑的灵活标签组件,适用于内容分类、筛选和标记。
标签组件演示
样式变体
标准标签
设计
开发
产品
营销
可移除标签
前端开发
JavaScript
CSS
HTML
彩色标签
重要
成功
警告
错误
信息
圆角标签
设计
开发
产品
营销
测试
大小变体
小型
标准
大型
交互式标签输入
提示:输入标签后按Enter键或点击添加按钮确认
标签组(可选择)
选择你的技能:
动画效果
淡入淡出
弹跳
脉冲
组件特点
- 多种样式变体(标准、彩色、圆角、大小)
- 支持添加和删除标签功能
- 可选的交互标签(可点击选择/取消选择)
- 平滑的动画过渡效果
- 响应式设计,适应不同屏幕尺寸
- 深色模式支持
- 完整的键盘可访问性支持
- 自定义颜色和主题
使用示例代码
<!-- 基本标签HTML结构 -->
<div class="tag-container">
<span class="tag tag-primary">前端开发</span>
<span class="tag tag-removable">
JavaScript
<button class="tag-remove-btn" aria-label="移除标签">×</button>
</span>
</div>
<!-- 交互式标签输入 -->
<div class="tag-input-container">
<div id="dynamic-tags" class="tag-container">
<!-- 动态添加的标签将显示在这里 -->
</div>
<input type="text" id="tag-input" placeholder="添加标签" />
</div>
详细使用指南
组件API
CSS类参考
| 类名 | 描述 | 应用场景 |
|---|---|---|
| .tag | 基础标签样式 | 所有类型的标签 |
| .tag-container | 标签容器样式 | 标签组的父容器 |
| .tag-removable | 可移除标签样式 | 需要用户可以移除的标签 |
| .tag-selectable | 可选择标签样式 | 多选场景的标签 |
| .tag-selected | 已选择标签样式 | 标记已选中的可选择标签 |
| .tag-primary/.tag-success/等 | 彩色标签变体 | 需要语义化颜色的标签 |
| .tag-pill | 圆角标签样式 | 需要圆形外观的标签 |
| .tag-small/.tag-large | 大小变体 | 需要不同尺寸的标签 |
| .tag-animate | 基本动画效果 | 需要简单悬停动画的标签 |
配置选项
CSS变量
您可以通过覆盖以下CSS变量来自定义标签组件的样式:
:root {
/* 颜色变量 */
--tag-bg-color: #f3f4f6; /* 标签背景色 */
--tag-text-color: #374151; /* 标签文字颜色 */
--tag-border-color: #e5e7eb; /* 标签边框颜色 */
--tag-hover-bg: #e5e7eb; /* 标签悬停背景色 */
--tag-active-bg: #d1d5db; /* 标签激活背景色 */
--tag-focus-ring: 0 0 0 3px rgba(67, 97, 238, 0.5); /* 标签聚焦效果 */
/* 尺寸变量 */
--tag-padding: 0.35rem 0.75rem; /* 标签内边距 */
--tag-radius: 0.375rem; /* 标签圆角 */
--tag-font-size: 0.875rem; /* 标签字体大小 */
/* 动画变量 */
--tag-transition: all 0.2s ease; /* 标签过渡动画 */
}
JavaScript API
核心函数
/**
* 添加新标签到指定容器
* @param {string} tagText - 标签文本内容
* @param {HTMLElement} container - 标签容器元素
* @param {boolean} removable - 是否可移除
* @returns {HTMLElement} - 创建的标签元素
*/
function addNewTag(tagText, container, removable = true) {
// 实现逻辑...
}
事件处理
- 点击移除按钮:触发标签移除动画并从DOM中删除
- Enter键:在输入框中按Enter键可添加新标签
- Ctrl+T (或 Cmd+T):快速聚焦到标签输入框
- 点击可选择标签:切换标签的选中状态
最佳实践
- 为提高可访问性,始终为标签添加适当的
aria-label属性 - 对于大量标签,考虑添加水平滚动功能或分页显示
- 实现标签重复检查,避免添加重复内容的标签
- 为移动设备优化标签大小和间距,确保触摸友好
- 使用语义化的彩色标签来传达不同的状态或类别
- 考虑添加标签排序或过滤功能,特别是在标签数量较多时
- 为标签输入添加自动完成功能,提高用户体验
性能优化建议
- 对于大量标签(100+),考虑使用虚拟滚动技术
- 使用事件委托来处理多个标签的点击事件,而不是为每个标签单独绑定事件
- 避免在标签动画期间进行重排操作,这会导致性能问题
- 使用CSS变量来统一管理样式,避免重复的样式定义
- 考虑使用节流(throttle)或防抖(debounce)技术来优化频繁的标签操作
无障碍支持
- 组件完全支持键盘导航,包括Tab键聚焦和Enter/Space操作
- 所有交互元素都有适当的
aria-属性 - 在高对比度模式下自动调整样式
- 支持
prefers-reduced-motion媒体查询,可在用户选择减少动画时禁用过渡效果 - 标签文本与背景的对比度符合WCAG AA标准
实际代码示例
以下是几个实际项目中使用标签组件的完整代码示例:
示例1: 基本标签管理系统
<!-- HTML结构 -->
<div class="tag-management-system">
<div class="tag-input-group">
<input type="text" id="user-tags-input" placeholder="添加标签...">
<button id="add-user-tag-btn">添加</button>
</div>
<div id="user-tags-container" class="tags-container"></div>
<div class="tag-actions">
<button id="save-tags-btn">保存标签</button>
<button id="clear-tags-btn">清空标签</button>
</div>
<div id="tags-status" class="status-message"></div>
</div>
<!-- JavaScript逻辑 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const tagInput = document.getElementById('user-tags-input');
const addBtn = document.getElementById('add-user-tag-btn');
const tagsContainer = document.getElementById('user-tags-container');
const saveBtn = document.getElementById('save-tags-btn');
const clearBtn = document.getElementById('clear-tags-btn');
const statusDiv = document.getElementById('tags-status');
// 添加新标签的函数
function addUserTag(tagText) {
// 验证输入
if (!tagText || !tagText.trim()) {
showStatus('请输入标签内容', 'warning');
return;
}
// 检查标签是否已存在
const existingTags = Array.from(tagsContainer.querySelectorAll('.tag')).map(tag =>
tag.textContent.replace('×', '').trim()
);
if (existingTags.includes(tagText.trim())) {
showStatus('标签已存在', 'warning');
return;
}
// 创建标签元素
const tagElement = document.createElement('span');
tagElement.className = 'tag tag-removable';
tagElement.innerHTML = `
${tagText.trim()}
`;
// 添加移除功能
const removeBtn = tagElement.querySelector('.tag-remove-btn');
removeBtn.addEventListener('click', function() {
tagElement.classList.add('tag-remove-animation');
setTimeout(() => {
tagElement.remove();
showStatus('标签已移除', 'success');
}, 300);
});
// 添加动画效果
tagElement.style.opacity = '0';
tagElement.style.transform = 'translateY(10px)';
tagsContainer.appendChild(tagElement);
// 触发重排,然后应用动画
void tagElement.offsetWidth;
tagElement.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
tagElement.style.opacity = '1';
tagElement.style.transform = 'translateY(0)';
// 清空输入框
tagInput.value = '';
tagInput.focus();
showStatus('标签已添加', 'success');
}
// 显示状态消息
function showStatus(message, type = 'info') {
statusDiv.textContent = message;
statusDiv.className = `status-message status-${type}`;
// 3秒后自动清除消息
setTimeout(() => {
statusDiv.textContent = '';
statusDiv.className = 'status-message';
}, 3000);
}
// 保存标签到本地存储
function saveTags() {
const tags = Array.from(tagsContainer.querySelectorAll('.tag')).map(tag =>
tag.textContent.replace('×', '').trim()
);
if (tags.length === 0) {
showStatus('没有可保存的标签', 'warning');
return;
}
localStorage.setItem('userTags', JSON.stringify(tags));
showStatus('标签已保存', 'success');
}
// 清空所有标签
function clearAllTags() {
if (tagsContainer.querySelectorAll('.tag').length === 0) {
showStatus('标签容器已为空', 'info');
return;
}
const tags = tagsContainer.querySelectorAll('.tag');
tags.forEach(tag => {
tag.classList.add('tag-remove-animation');
setTimeout(() => tag.remove(), 300);
});
showStatus('所有标签已清空', 'success');
}
// 从本地存储加载标签
function loadTags() {
const savedTags = localStorage.getItem('userTags');
if (savedTags) {
try {
const tags = JSON.parse(savedTags);
tags.forEach(tag => addUserTag(tag));
showStatus('已加载保存的标签', 'success');
} catch (e) {
showStatus('加载标签失败', 'danger');
}
}
}
// 事件监听
addBtn.addEventListener('click', () => addUserTag(tagInput.value));
tagInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
addUserTag(this.value);
}
});
saveBtn.addEventListener('click', saveTags);
clearBtn.addEventListener('click', clearAllTags);
// 加载保存的标签
loadTags();
// 键盘快捷键
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveTags();
} else if ((e.ctrlKey || e.metaKey) && e.key === 'd') {
e.preventDefault();
clearAllTags();
}
});
});
</script>
<!-- CSS样式 -->
<style>
.tag-management-system {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
border-radius: 8px;
}
.tag-input-group {
display: flex;
margin-bottom: 15px;
}
.tag-input-group input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
font-size: 14px;
}
.tag-input-group button {
padding: 10px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
.tag-input-group button:hover {
background-color: #45a049;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 10px;
min-height: 50px;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 15px;
}
.tag-actions {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.tag-actions button {
padding: 8px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
#save-tags-btn {
background-color: #2196F3;
color: white;
}
#save-tags-btn:hover {
background-color: #0b7dda;
}
#clear-tags-btn {
background-color: #f44336;
color: white;
}
#clear-tags-btn:hover {
background-color: #da190b;
}
.status-message {
padding: 10px;
border-radius: 4px;
font-size: 14px;
text-align: center;
}
.status-success {
background-color: #d4edda;
color: #155724;
}
.status-warning {
background-color: #fff3cd;
color: #856404;
}
.status-danger {
background-color: #f8d7da;
color: #721c24;
}
.status-info {
background-color: #d1ecf1;
color: #0c5460;
}
</style>
这个示例展示了一个完整的标签管理系统,包括添加、移除、保存和加载标签的功能,带有状态提示和键盘快捷键支持。
示例2: 使用TypeScript和React的标签组件集成
import React, { useState, useRef, useEffect } from 'react';
import './TagComponent.css';
interface Tag {
id: string;
text: string;
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
}
interface TagInputProps {
initialTags?: Tag[];
onTagsChange?: (tags: Tag[]) => void;
placeholder?: string;
maxTags?: number;
allowedTypes?: ('primary' | 'success' | 'warning' | 'danger' | 'info')[];
}
const TagInput: React.FC = ({
initialTags = [],
onTagsChange,
placeholder = '添加标签...',
maxTags = 10,
allowedTypes = ['primary', 'success', 'warning', 'danger', 'info']
}) => {
const [tags, setTags] = useState(initialTags);
const [inputValue, setInputValue] = useState('');
const [selectedType, setSelectedType] = useState<'primary' | 'success' | 'warning' | 'danger' | 'info'>('primary');
const inputRef = useRef(null);
// 通知父组件标签变化
useEffect(() => {
if (onTagsChange) {
onTagsChange(tags);
}
}, [tags, onTagsChange]);
// 生成唯一ID
const generateId = (): string => {
return `tag-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
};
// 检查标签是否已存在
const isTagExists = (text: string): boolean => {
return tags.some(tag => tag.text.toLowerCase() === text.toLowerCase().trim());
};
// 添加标签
const addTag = (text: string): void => {
const trimmedText = text.trim();
if (!trimmedText || isTagExists(trimmedText) || tags.length >= maxTags) {
return;
}
const newTag: Tag = {
id: generateId(),
text: trimmedText,
type: selectedType
};
setTags([...tags, newTag]);
setInputValue('');
// 保持输入框聚焦
if (inputRef.current) {
inputRef.current.focus();
}
};
// 移除标签
const removeTag = (id: string): void => {
setTags(tags.filter(tag => tag.id !== id));
};
// 处理输入按键事件
const handleKeyDown = (e: React.KeyboardEvent): void => {
if (e.key === 'Enter') {
e.preventDefault();
addTag(inputValue);
} else if (e.key === 'Backspace' && !inputValue && tags.length > 0) {
// 支持按退格键删除最后一个标签
setTags(tags.slice(0, -1));
}
};
// 处理输入变化
const handleInputChange = (e: React.ChangeEvent): void => {
setInputValue(e.target.value);
};
// 键盘快捷键支持
useEffect(() => {
const handleGlobalKeyDown = (e: KeyboardEvent): void => {
if ((e.ctrlKey || e.metaKey) && e.key === 't' && !e.shiftKey) {
e.preventDefault();
if (inputRef.current) {
inputRef.current.focus();
}
}
};
window.addEventListener('keydown', handleGlobalKeyDown);
return () => window.removeEventListener('keydown', handleGlobalKeyDown);
}, []);
return (
{tags.map(tag => (
{tag.text}
))}
= maxTags}
className="tag-input-field"
role="combobox"
aria-autocomplete="list"
aria-expanded="false"
aria-controls="dynamic-tags"
/>
{tags.length >= maxTags && (
已达到最大标签数量 ({maxTags})
)}
);
};
export default TagInput;
这个示例展示了如何在React和TypeScript项目中集成标签组件,包括类型定义、状态管理、键盘导航和无障碍支持等特性。
示例3: 带有搜索和过滤功能的标签系统
// HTML结构
// <div class="advanced-tag-system">
// <div class="search-input-group">
// <input type="text" id="tag-search-input" placeholder="搜索标签...">
// <select id="tag-filter">
// <option value="all">全部类型</option>
// <option value="primary">主要</option>
// <option value="success">成功</option>
// <option value="warning">警告</option>
// <option value="danger">危险</option>
// <option value="info">信息</option>
// </select>
// </div>
// <div class="available-tags">
// <h4>可用标签</h4>
// <div id="available-tags-container" class="tags-container"></div>
// </div>
// <div class="selected-tags">
// <h4>已选标签</h4>
// <div id="selected-tags-container" class="tags-container"></div>
// </div>
// </div>
// JavaScript代码
class AdvancedTagSystem {
constructor() {
// 元素引用
this.searchInput = document.getElementById('tag-search-input');
this.tagFilter = document.getElementById('tag-filter');
this.availableTagsContainer = document.getElementById('available-tags-container');
this.selectedTagsContainer = document.getElementById('selected-tags-container');
// 数据
this.availableTags = [];
this.selectedTags = [];
// 初始化
this.init();
}
// 初始化函数
init() {
// 加载可用标签数据
this.loadAvailableTags();
// 添加事件监听器
this.searchInput.addEventListener('input', this.handleSearch.bind(this));
this.tagFilter.addEventListener('change', this.handleFilter.bind(this));
// 添加键盘导航支持
this.setupKeyboardNavigation();
}
// 加载可用标签数据
loadAvailableTags() {
// 模拟从API获取数据
const mockTags = [
{ id: 1, text: 'JavaScript', type: 'primary' },
{ id: 2, text: 'TypeScript', type: 'success' },
{ id: 3, text: 'React', type: 'primary' },
{ id: 4, text: 'Vue', type: 'success' },
{ id: 5, text: 'Angular', type: 'warning' },
{ id: 6, text: 'Node.js', type: 'danger' },
{ id: 7, text: 'Express', type: 'info' },
{ id: 8, text: 'Next.js', type: 'primary' },
{ id: 9, text: 'Nuxt.js', type: 'success' },
{ id: 10, text: 'GraphQL', type: 'warning' }
];
this.availableTags = mockTags;
this.renderAvailableTags();
}
// 渲染可用标签
renderAvailableTags() {
// 清空容器
this.availableTagsContainer.innerHTML = '';
// 获取搜索词和筛选条件
const searchTerm = this.searchInput.value.toLowerCase();
const filterType = this.tagFilter.value;
// 筛选标签
const filteredTags = this.availableTags.filter(tag => {
const matchesSearch = tag.text.toLowerCase().includes(searchTerm);
const matchesFilter = filterType === 'all' || tag.type === filterType;
const notSelected = !this.selectedTags.some(selected => selected.id === tag.id);
return matchesSearch && matchesFilter && notSelected;
});
// 渲染标签
filteredTags.forEach(tag => {
const tagElement = this.createTagElement(tag, true);
this.availableTagsContainer.appendChild(tagElement);
});
// 如果没有结果,显示提示信息
if (filteredTags.length === 0) {
const noResults = document.createElement('div');
noResults.className = 'no-results';
noResults.textContent = '没有找到匹配的标签';
this.availableTagsContainer.appendChild(noResults);
}
}
// 渲染已选标签
renderSelectedTags() {
// 清空容器
this.selectedTagsContainer.innerHTML = '';
// 渲染标签
this.selectedTags.forEach(tag => {
const tagElement = this.createTagElement(tag, false);
this.selectedTagsContainer.appendChild(tagElement);
});
// 如果没有选择标签,显示提示信息
if (this.selectedTags.length === 0) {
const noSelection = document.createElement('div');
noSelection.className = 'no-selection';
noSelection.textContent = '请从上方选择标签';
this.selectedTagsContainer.appendChild(noSelection);
}
}
// 创建标签元素
createTagElement(tag, isAvailable) {
const tagElement = document.createElement('span');
tagElement.className = `tag tag-${tag.type} tag-selectable`;
tagElement.setAttribute('data-tag-id', tag.id);
tagElement.setAttribute('role', 'button');
tagElement.setAttribute('tabindex', '0');
tagElement.setAttribute('aria-label', `标签: ${tag.text}`);
// 设置标签文本
tagElement.textContent = tag.text;
// 添加点击事件
if (isAvailable) {
tagElement.addEventListener('click', () => this.selectTag(tag));
} else {
tagElement.addEventListener('click', () => this.deselectTag(tag.id));
// 添加移除按钮
const removeBtn = document.createElement('button');
removeBtn.className = 'tag-remove-btn';
removeBtn.setAttribute('aria-label', `移除标签 ${tag.text}`);
removeBtn.innerHTML = '×';
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.deselectTag(tag.id);
});
tagElement.appendChild(removeBtn);
tagElement.classList.add('tag-removable', 'tag-selected');
}
// 添加键盘支持
tagElement.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
tagElement.click();
}
});
return tagElement;
}
// 选择标签
selectTag(tag) {
// 检查标签是否已被选择
if (this.selectedTags.some(selected => selected.id === tag.id)) {
return;
}
// 添加到已选标签
this.selectedTags.push(tag);
// 更新UI
this.renderAvailableTags();
this.renderSelectedTags();
// 触发自定义事件
const selectEvent = new CustomEvent('tagSelect', {
detail: { tag }
});
document.dispatchEvent(selectEvent);
}
// 取消选择标签
deselectTag(tagId) {
// 从已选标签中移除
this.selectedTags = this.selectedTags.filter(tag => tag.id !== tagId);
// 更新UI
this.renderAvailableTags();
this.renderSelectedTags();
// 触发自定义事件
const deselectEvent = new CustomEvent('tagDeselect', {
detail: { tagId }
});
document.dispatchEvent(deselectEvent);
}
// 处理搜索
handleSearch() {
this.renderAvailableTags();
}
// 处理筛选
handleFilter() {
this.renderAvailableTags();
}
// 设置键盘导航
setupKeyboardNavigation() {
// 搜索输入框聚焦
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
this.searchInput.focus();
}
});
// 使用Tab和Shift+Tab在标签间导航
this.availableTagsContainer.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
const tags = Array.from(this.availableTagsContainer.querySelectorAll('.tag'));
const currentIndex = tags.indexOf(document.activeElement);
if (e.shiftKey) {
// 向前导航
if (currentIndex > 0) {
e.preventDefault();
tags[currentIndex - 1].focus();
}
} else {
// 向后导航
if (currentIndex < tags.length - 1) {
e.preventDefault();
tags[currentIndex + 1].focus();
}
}
}
});
}
// 获取已选标签
getSelectedTags() {
return [...this.selectedTags];
}
// 清空已选标签
clearSelectedTags() {
this.selectedTags = [];
this.renderAvailableTags();
this.renderSelectedTags();
}
}
// 初始化高级标签系统
const tagSystem = new AdvancedTagSystem();
这个示例展示了一个带有搜索、过滤和分类功能的高级标签系统,采用面向对象的方式实现,可以轻松集成到各种项目中。