Skip to content

Instantly share code, notes, and snippets.

@enesien
Last active June 24, 2024 06:34
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

@code-guerilla
Copy link

Nice thank you appreciate your work alot !

I am using Server Actions, with html/tailwind validation, and the Formdata. It would be nice. If you could add a Server Action version.

In the moment i just use a standart html select with multiple attributes. But cant get it pretty accessible enough to fit with the shadcn style.

@enesien
Copy link
Author

enesien commented Oct 13, 2023

Nice thank you appreciate your work alot !

I am using Server Actions, with html/tailwind validation, and the Formdata. It would be nice. If you could add a Server Action version.

In the moment i just use a standart html select with multiple attributes. But cant get it pretty accessible enough to fit with the shadcn style.

Thanks!

Don't you just call the server action from the submit handler function wrapped in startTransition?

const onSubmit = async (values: z.infer<typeof schema>) => {
  startTransition(() => {
    someServerAction(values);
  });
}

@agreen254
Copy link

Great work @enesien! Helped me better understand forwarding refs with react hook form.

@puneetv05
Copy link

@enesien Thank you :)

@Ritiksh0h
Copy link

Great work! Just add
InputTags.displayName = "InputTags"; export default InputTags;
at the end of component;; otherwise, it will give error Error: Component definition is missing display name react/display-name

@Kavindu-Wijesekara
Copy link

@enesien thank you <3

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