Multi-tag Input
Create an input components for submitting multiple tags using Shadcn/ui form components.
Demo

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:
npx shadcn-ui@latest add badge button form input labelCode
11 collapsed lines
1import { Badge } from "@/components/ui/badge";2import { Button } from "@/components/ui/button";3import { Input, InputProps } from "@/components/ui/input";4import { XIcon } from "lucide-react";5import { Dispatch, SetStateAction, forwardRef, useState } from "react";6
7type InputTagsProps = InputProps & {8 value: string[];9 onChange: Dispatch<SetStateAction<string[]>>;10};11
12export 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 <Input32 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 <Button48 type="button"49 variant="secondary"50 className="rounded-l-none border border-l-0"51 onClick={addPendingDataPoint}52 >53 Add54 </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 <button61 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
12 collapsed lines
1import { zodResolver } from "@hookform/resolvers/zod";2import { useForm } from "react-hook-form";3import { z } from "zod";4import {5 Form,6 FormField,7 FormItem,8 FormLabel,9 FormMessage,10} from "@/components/ui/form";11import { InputTags } from "./MultiTagInput";12
13// Define form schema14const formSchema = z.object({15 skills: z.array(z.string().trim()).max(20), // edit as needed16});17
18export default function App() {19 // Initialize form20 const form = useForm<z.infer<typeof formSchema>>({21 resolver: zodResolver(formSchema),22 defaultValues: {23 skills: [],24 },25 });26
27 // Handle form submission28 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 <FormField38 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}