Collaboration
Project Management
Finance
Tags
Tags are a way to apply multiple labels to an item.
Installation
npx love-ui@latest add tagsUsage
import { Tags, TagsTrigger, TagsValue, TagsContent, TagsInput, TagsList, TagsEmpty, TagsGroup, TagsItem } from "@/components/tags"<Tags availableTags={availableTags}>
<TagsTrigger>
{selectedTags.map((tag) => (
<TagsValue key={tag.id}>{tag.name}</TagsValue>
))}
</TagsTrigger>
<TagsContent>
<TagsInput placeholder="Search tags..." />
<TagsList>
<TagsEmpty>No tags found.</TagsEmpty>
<TagsGroup>
{availableTags.map((tag) => (
<TagsItem key={tag.id} value={tag.id}>{tag.name}</TagsItem>
))}
</TagsGroup>
</TagsList>
</TagsContent>
</Tags>Features
- Built-in search input to filter through tags
- Supports both controlled and uncontrolled usage
- Fully customizable through className props
- Automatically adjusts width based on parent container
- Optional remove button for selected tags
- Full keyboard navigation support through Command component
- Shared state management through TagsContext
Examples
Create a tag
"use client";
import {
Tags,
TagsContent,
TagsEmpty,
TagsGroup,
TagsInput,
TagsItem,
TagsList,
TagsTrigger,
TagsValue,
} from "../../../../../packages/tags";
import { CheckIcon, PlusIcon } from "lucide-react";
import { useState } from "react";
const defaultTags = [
{ id: "react", label: "React" },
{ id: "typescript", label: "TypeScript" },
{ id: "javascript", label: "JavaScript" },
{ id: "nextjs", label: "Next.js" },
{ id: "vuejs", label: "Vue.js" },
{ id: "angular", label: "Angular" },
{ id: "svelte", label: "Svelte" },
{ id: "nodejs", label: "Node.js" },
{ id: "python", label: "Python" },
{ id: "ruby", label: "Ruby" },
{ id: "java", label: "Java" },
{ id: "csharp", label: "C#" },
{ id: "php", label: "PHP" },
{ id: "go", label: "Go" },
];
const Example = () => {
const [selected, setSelected] = useState<string[]>([]);
const [newTag, setNewTag] = useState<string>("");
const [tags, setTags] =
useState<{ id: string; label: string }[]>(defaultTags);
const handleRemove = (value: string) => {
if (!selected.includes(value)) {
return;
}
console.log(`removed: ${value}`);
setSelected((prev) => prev.filter((v) => v !== value));
};
const handleSelect = (value: string) => {
if (selected.includes(value)) {
handleRemove(value);
return;
}
console.log(`selected: ${value}`);
setSelected((prev) => [...prev, value]);
};
const handleCreateTag = () => {
console.log(`created: ${newTag}`);
setTags((prev) => [
...prev,
{
id: newTag,
label: newTag,
},
]);
setSelected((prev) => [...prev, newTag]);
setNewTag("");
};
return (
<Tags className="max-w-[300px]">
<TagsTrigger>
{selected.map((tag) => (
<TagsValue key={tag} onRemove={() => handleRemove(tag)}>
{tags.find((t) => t.id === tag)?.label}
</TagsValue>
))}
</TagsTrigger>
<TagsContent>
<TagsInput onValueChange={setNewTag} placeholder="Search tag..." />
<TagsList>
<TagsEmpty>
<button
className="mx-auto flex cursor-pointer items-center gap-2"
onClick={handleCreateTag}
type="button"
>
<PlusIcon className="text-muted-foreground" size={14} />
Create new tag: {newTag}
</button>
</TagsEmpty>
<TagsGroup>
{tags.map((tag) => (
<TagsItem key={tag.id} onSelect={handleSelect} value={tag.id}>
{tag.label}
{selected.includes(tag.id) && (
<CheckIcon className="text-muted-foreground" size={14} />
)}
</TagsItem>
))}
</TagsGroup>
</TagsList>
</TagsContent>
</Tags>
);
};
export default Example;
Filter available tags
"use client";
import {
Tags,
TagsContent,
TagsEmpty,
TagsGroup,
TagsInput,
TagsItem,
TagsList,
TagsTrigger,
TagsValue,
} from "../../../../../packages/tags";
import { useState } from "react";
const defaultTags = [
{ id: "react", label: "React" },
{ id: "typescript", label: "TypeScript" },
{ id: "javascript", label: "JavaScript" },
{ id: "nextjs", label: "Next.js" },
{ id: "vuejs", label: "Vue.js" },
{ id: "angular", label: "Angular" },
{ id: "svelte", label: "Svelte" },
{ id: "nodejs", label: "Node.js" },
{ id: "python", label: "Python" },
{ id: "ruby", label: "Ruby" },
{ id: "java", label: "Java" },
{ id: "csharp", label: "C#" },
{ id: "php", label: "PHP" },
{ id: "go", label: "Go" },
];
const Example = () => {
const [selected, setSelected] = useState<string[]>([]);
const [newTag, setNewTag] = useState<string>("");
const [tags, setTags] =
useState<{ id: string; label: string }[]>(defaultTags);
const handleRemove = (value: string) => {
if (!selected.includes(value)) {
return;
}
console.log(`removed: ${value}`);
setSelected((prev) => prev.filter((v) => v !== value));
};
const handleSelect = (value: string) => {
if (selected.includes(value)) {
handleRemove(value);
return;
}
console.log(`selected: ${value}`);
setSelected((prev) => [...prev, value]);
};
return (
<Tags className="max-w-[300px]">
<TagsTrigger>
{selected.map((tag) => (
<TagsValue key={tag} onRemove={() => handleRemove(tag)}>
{tags.find((t) => t.id === tag)?.label}
</TagsValue>
))}
</TagsTrigger>
<TagsContent>
<TagsInput onValueChange={setNewTag} placeholder="Search tag..." />
<TagsList>
<TagsEmpty />
<TagsGroup>
{tags
.filter((tag) => !selected.includes(tag.id))
.map((tag) => (
<TagsItem key={tag.id} onSelect={handleSelect} value={tag.id}>
{tag.label}
</TagsItem>
))}
</TagsGroup>
</TagsList>
</TagsContent>
</Tags>
);
};
export default Example;