Skip to content

Instantly share code, notes, and snippets.

@enesien
Last active October 4, 2024 03:10
Show Gist options
  • Save enesien/03ba5340f628c6c812b306da5fedd1a4 to your computer and use it in GitHub Desktop.
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

@algsupport
Copy link

@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
Copy link

destpat commented Aug 29, 2024

Work great thank you 👍 💯

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