Skip to content

Instantly share code, notes, and snippets.

@enesien
Last active April 8, 2026 18:59
Show Gist options
  • Select an option

  • Save enesien/03ba5340f628c6c812b306da5fedd1a4 to your computer and use it in GitHub Desktop.

Select an option

Save enesien/03ba5340f628c6c812b306da5fedd1a4 to your computer and use it in GitHub Desktop.
shadcn multiple tag input

shadcn/ui multi tag input component

A react tag input field component using shadcn, like one you see when adding keywords to a video on YouTube. Usable with Form or standalone.

Preview

image

Standalone Usage

const [values, setValues] = useState<string[]>([])
...
<InputTags value={values} onChange={setValues} />

Form Usage (React Hook Form)

<FormField
  control={form.control}
  name="data_points"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Add Data Point(s)</FormLabel>
      <FormControl>
        <InputTags {...field} />
      </FormControl>
      <FormDescription>
       ...
      </FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

Component

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input, InputProps } from "@/components/ui/input";
import { XIcon } from "lucide-react";
import { Dispatch, SetStateAction, forwardRef, useState } from "react";

type InputTagsProps = InputProps & {
  value: string[];
  onChange: Dispatch<SetStateAction<string[]>>;
};

export const InputTags = forwardRef<HTMLInputElement, InputTagsProps>(
  ({ value, onChange, ...props }, ref) => {
    const [pendingDataPoint, setPendingDataPoint] = useState("");

    const addPendingDataPoint = () => {
      if (pendingDataPoint) {
        const newDataPoints = new Set([...value, pendingDataPoint]);
        onChange(Array.from(newDataPoints));
        setPendingDataPoint("");
      }
    };

    return (
      <>
        <div className="flex">
          <Input
            value={pendingDataPoint}
            onChange={(e) => setPendingDataPoint(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === "Enter") {
                e.preventDefault();
                addPendingDataPoint();
              } else if (e.key === "," || e.key === " ") {
                e.preventDefault();
                addPendingDataPoint();
              }
            }}
            className="rounded-r-none"
            {...props}
            ref={ref}
          />
          <Button
            type="button"
            variant="secondary"
            className="rounded-l-none border border-l-0"
            onClick={addPendingDataPoint}
          >
            Add
          </Button>
        </div>
        <div className="border rounded-md min-h-[2.5rem] overflow-y-auto p-2 flex gap-2 flex-wrap items-center">
          {value.map((item, idx) => (
            <Badge key={idx} variant="secondary">
              {item}
              <button
                type="button"
                className="w-3 ml-2"
                onClick={() => {
                  onChange(value.filter((i) => i !== item));
                }}
              >
                <XIcon className="w-3" />
              </button>
            </Badge>
          ))}
        </div>
      </>
    );
  }
);

Godspeed!

⬑ Created by Enesien

@algdev-gr

Copy link
Copy Markdown

@aryanbhat One note regarding your change. This will have the effect of changing the form height when the field appears and in many cases when you have other things under the form, this may not be deisrable.
I would make the function optional with a default value in order to preserve backward compatibility.
Just a suggestion.

@destpat

destpat commented Aug 29, 2024

Copy link
Copy Markdown

Work great thank you πŸ‘ πŸ’―

@kimuradev

Copy link
Copy Markdown

Good job mate!

@aryanbhat

Copy link
Copy Markdown

@algsupport Yeah got it mate took some time to understand 🀣

@abdulwaheed-apk

Copy link
Copy Markdown

πŸ‘

@Iqbalhmbl

Copy link
Copy Markdown

πŸ‘

@prasathmani

Copy link
Copy Markdown

thanks, it saved my time. πŸ‘

@zessu

zessu commented Apr 8, 2026

Copy link
Copy Markdown

This is πŸ”₯πŸ”₯πŸ”₯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment