Skip to content

Multi-tag Input

Create an input components for submitting multiple tags using Shadcn/ui form components.

Demo

Multi-tag input component

Prerequisites

Shadcn/ui, lucide-react, zod, and react-hook-form are required to use the code snippet below.

Make sure you add the required components to your project:

Terminal window
npx shadcn-ui@latest add badge button form input label

Code

MultiTagInput.tsx
11 collapsed lines
1
import { Badge } from "@/components/ui/badge";
2
import { Button } from "@/components/ui/button";
3
import { Input, InputProps } from "@/components/ui/input";
4
import { XIcon } from "lucide-react";
5
import { Dispatch, SetStateAction, forwardRef, useState } from "react";
6
7
type InputTagsProps = InputProps & {
8
value: string[];
9
onChange: Dispatch<SetStateAction<string[]>>;
10
};
11
12
export const InputTags = forwardRef<HTMLInputElement, InputTagsProps>(
13
({ value, onChange, ...props }, ref) => {
14
const [pendingDataPoint, setPendingDataPoint] = useState("");
15
16
if (!value) {
17
value = [];
18
}
19
20
const addPendingDataPoint = () => {
21
if (pendingDataPoint) {
22
const newDataPoints = new Set([...value, pendingDataPoint]);
23
onChange(Array.from(newDataPoints));
24
setPendingDataPoint("");
25
}
26
};
27
28
return (
29
<>
30
<div className="flex">
31
<Input
32
value={pendingDataPoint}
33
onChange={(e) => setPendingDataPoint(e.target.value)}
34
onKeyDown={(e) => {
35
if (e.key === "Enter") {
36
e.preventDefault();
37
addPendingDataPoint();
38
} else if (e.key === "," || e.key === " ") {
39
e.preventDefault();
40
addPendingDataPoint();
41
}
42
}}
43
className="rounded-r-none w-80"
44
{...props}
45
ref={ref}
46
/>
47
<Button
48
type="button"
49
variant="secondary"
50
className="rounded-l-none border border-l-0"
51
onClick={addPendingDataPoint}
52
>
53
Add
54
</Button>
55
</div>
56
<div className="border rounded-md min-h-[2.5rem] overflow-y-auto p-2 flex gap-2 flex-wrap items-center">
57
{value.map((item, idx) => (
58
<Badge key={idx} variant="secondary">
59
{item}
60
<button
61
type="button"
62
className="w-3 ml-2"
63
onClick={() => {
64
onChange(value.filter((i) => i !== item));
65
}}
66
>
67
<XIcon className="w-3" />
68
</button>
69
</Badge>
70
))}
71
</div>
72
</>
73
);
74
}
75
);

Usage

App.tsx
12 collapsed lines
1
import { zodResolver } from "@hookform/resolvers/zod";
2
import { useForm } from "react-hook-form";
3
import { z } from "zod";
4
import {
5
Form,
6
FormField,
7
FormItem,
8
FormLabel,
9
FormMessage,
10
} from "@/components/ui/form";
11
import { InputTags } from "./MultiTagInput";
12
13
// Define form schema
14
const formSchema = z.object({
15
skills: z.array(z.string().trim()).max(20), // edit as needed
16
});
17
18
export default function App() {
19
// Initialize form
20
const form = useForm<z.infer<typeof formSchema>>({
21
resolver: zodResolver(formSchema),
22
defaultValues: {
23
skills: [],
24
},
25
});
26
27
// Handle form submission
28
const onSubmit = (data: z.infer<typeof formSchema>) => {
29
// Do something with the form values.
30
// ✅ This will be type-safe and validated.
31
console.log(data);
32
};
33
34
return (
35
<Form {...form}>
36
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
37
<FormField
38
control={form.control}
39
name="skills"
40
render={({ field }) => (
41
<FormItem>
42
<FormLabel>Skills</FormLabel>
43
<FormControl>
44
<InputTags {...field} />
45
</FormControl>
46
<FormMessage />
47
</FormItem>
48
)}
49
/>
50
</form>
51
</Form>
52
);
53
}