Meez.
Browse Prompts Backgrounds Pricing
Contact Sign in ·

Form · Free prompt

Tag Input

free · paste into Lovable, Bolt, v0, Cursor or Claude

Tag Input is a complete, paste-ready form build-prompt — a full art-direction spec for Lovable, Bolt, v0, Cursor and Claude. It's free: copy it below and paste it into your builder.

Tag Input preview

“A sophisticated tag input component with suggestions, autocomplete, and custom styling. Built with React and Tailwind CSS, supporting both light and dark themes. Features - Tag Management - Add/remove tags - Maximum …”

Tag InputForm · free prompt · copy-paste

Prompt page

A sophisticated tag input component with suggestions, autocomplete, and custom styling. Built with React and Tailwind CSS, supporting both light and dark themes.

Features

- Tag Management
  - Add/remove tags
  - Maximum tags limit
  - Custom tag colors
  - Default tags support
  - Tag suggestions

- Interactive UI
  - Autocomplete dropdown
  - Keyboard navigation
  - Clear button for each tag
  - Custom tag styling
  - Smooth animations

— Install —
npx shadcn@latest add "https://21st.dev/r/tag-input"

— Component source (Tag Input.tsx) —

"use client";

import { useState, useRef } from "react";
import { cn } from "@/lib/utils";
import { useTags } from "@/components/hooks/use-tags";
import { useOnClickOutside } from "@/components/hooks/use-click-outside";
import { Cross2Icon, CheckIcon, PlusIcon } from "@radix-ui/react-icons";

interface Tag {
  id: string;
  label: string;
  color?: string;
}

interface TagInputProps {
  onChange?: (tags: Array<Tag>) => void;
  defaultTags?: Array<Tag>;
  suggestions?: Array<Tag>;
  maxTags?: number;
  label?: string;
  placeholder?: string;
  error?: string;
}
const defaultKokonutTag: Tag = {
  id: "kokonut-ui",
  label: "KokonutUI",
  color:
    "bg-indigo-100 text-indigo-700 border border-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-300 dark:border-indigo-700/30",
};

const tagStyles = {
  base: "inline-flex items-center gap-1.5 px-2 py-0.5 text-sm rounded-md transition-colors duration-150",
  colors: {
    blue: "bg-blue-50 text-blue-700 border border-blue-200 hover:border-blue-300 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700/30 dark:hover:border-blue-600/50",
    purple:
      "bg-purple-50 text-purple-700 border border-purple-200 hover:border-purple-300 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-700/30 dark:hover:border-purple-600/50",
    green:
      "bg-green-50 text-green-700 border border-green-200 hover:border-green-300 dark:bg-green-900/30 dark:text-green-300 dark:border-green-700/30 dark:hover:border-green-600/50",
  },
};

function TagInput({
  onChange,
  defaultTags = [defaultKokonutTag], // Set KokonutUI as default
  suggestions = [
    { id: "nextjs", label: "Next.js" },
    { id: "react", label: "React" },
    { id: "tailwind", label: "Tailwind" },
  ],
  maxTags = 10,
  label = "Tags",
  placeholder = "Add tags...",
  error,
}: TagInputProps) {
  const { tags, addTag, removeTag, removeLastTag } = useTags({
    onChange,
    defaultTags,
    maxTags,
  });
  const [input, setInput] = useState("");
  const [isOpen, setIsOpen] = useState(false);
  const [selectedIndex, setSelectedIndex] = useState(0);
  const inputRef = useRef<HTMLInputElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  const filteredSuggestions = suggestions
    .filter(
      (suggestion: Tag) =>
        typeof suggestion.label === "string" &&
        typeof input === "string" &&
        suggestion.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 &&
        !tags.some((tag: Tag) => tag.id === suggestion.id)
    )
    .slice(0, 5);

  const canAddNewTag =
    input.length > 0 &&
    !suggestions.some((s) => s.label.toLowerCase() === input.toLowerCase());

  function handleKeyDown(e: React.KeyboardEvent) {
    if (e.key === "Backspace" && input === "" && tags.length > 0) {
      removeLastTag();
    } else if (e.key === "Enter" && input) {
      e.preventDefault();
      if (isOpen && filteredSuggestions[selectedIndex]) {
        addTag(filteredSuggestions[selectedIndex]);
        setInput("");
        setIsOpen(false);
      } else if (canAddNewTag) {
        addTag({ id: input, label: input });
        setInput("");
        setIsOpen(false);
      }
    } else if (e.key === "Escape") {
      setIsOpen(false);
    }
  }

  useOnClickOutside(containerRef, () => setIsOpen(false));

  return (
    <div
      className="w-full max-w-full sm:max-w-2xl space-y-2"
      ref={containerRef}
    >
      {label && (
        <label
          className="text-sm font-medium text-zinc-800 dark:text-zinc-200"
          htmlFor={label}
        >
          {label}
        </label>
      )}

      <div
        className={cn(
          "min-h-[3rem] sm:min-h-[2.5rem] p-2 sm:p-1.5",
          "rounded-lg border",
          "border-zinc-300 dark:border-zinc-700",
          "bg-white dark:bg-zinc-900",
          "focus-within:ring-2 focus-within:ring-indigo-500/30 dark:focus-within:ring-indigo-400/30",
          "flex items-center flex-row flex-wrap gap-2 sm:gap-1.5 relative"
        )}
      >
        {tags.map((tag) => (
          <span
            key={tag.id}
            className={cn(
              tagStyles.base,
              "text-base sm:text-sm py-1 sm:py-0.5",
              tag.color || tagStyles.colors.blue
            )}
          >
            {tag.label}
            <button
              type="button"
              onClick={() => removeTag(tag.id)}
              className={cn(
                "text-current/60 hover:text-current transition-colors",
                "p-1 sm:p-0"
              )}
            >
              <Cross2Icon className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
            </button>
          </span>
        ))}

        <input
          ref={inputRef}
          type="text"
          value={input}
          onChange={(e) => {
            setInput(e.target.value);
            setIsOpen(true);
            setSelectedIndex(0);
          }}
          onFocus={() => setIsOpen(true)}
          onKeyDown={handleKeyDown}
          placeholder={tags.length === 0 ? placeholder : ""}
          className={cn(
            "flex-1 min-w-[140px] sm:min-w-[120px] bg-transparent",
            "h-8 sm:h-7",
            "text-base sm:text-sm",
            "text-zinc-900 dark:text-zinc-100",
            "placeholder:text-zinc-500 dark:placeholder:text-zinc-400",
            "focus:outline-none"
          )}
        />

        {isOpen && (input || filteredSuggestions.length > 0) && (
          <div
            className={cn(
              "absolute left-0 right-0 top-full mt-1 z-50",
              "max-h-[60vh] sm:max-h-[300px] overflow-y-auto",
              "bg-white dark:bg-zinc-900",
              "border border-zinc-300 dark:border-zinc-700",
              "rounded-lg shadow-lg dark:shadow-zinc-950/50",
              "overflow-hidden"
            )}
          >
            <div className="px-2 py-1.5 border-b border-zinc-200 dark:border-zinc-800">
              <span className="text-xs font-medium text-zinc-600 dark:text-zinc-300">
                Choose a tag or create one
              </span>
            </div>
            <div className="p-2 sm:p-1.5 flex flex-wrap gap-2 sm:gap-1.5">
              {filteredSuggestions.map((suggestion, index) => (
                <button
                  type="button"
                  key={suggestion.id}
                  onClick={() => {
                    addTag(suggestion);
                    setInput("");
                    setIsOpen(false);
                  }}
                  className={cn(
                    tagStyles.base,
                    selectedIndex === index
                      ? tagStyles.colors.blue
                      : "bg-zinc-50 text-zinc-700 border border-zinc-300 hover:border-zinc-400 dark:bg-zinc-800/50 dark:text-zinc-300 dark:border-zinc-700 dark:hover:border-zinc-600"
                  )}
                >
                  {suggestion.label}
                  {selectedIndex === index && (
                    <CheckIcon className="w-3.5 h-3.5" />
                  )}
                </button>
              ))}
              {canAddNewTag && (
                <button
                  type="button"
                  onClick={() => {
                    const colorKeys = Object.keys(tagStyles.colors) as Array<
                      keyof typeof tagStyles.colors
                    >;
                    const randomColor =
                      tagStyles.colors[
                        colorKeys[Math.floor(Math.random() * colorKeys.length)]
                      ];
                    addTag({
                      id: input,
                      label: input,
                      color: randomColor,
                    });
                    setInput("");
                    setIsOpen(false);
                  }}
                  className={cn(
                    tagStyles.base,
                    selectedIndex === filteredSuggestions.length
                      ? tagStyles.colors.blue
                      : "bg-zinc-50 text-zinc-700 border border-zinc-300 hover:border-zinc-400 dark:bg-zinc-800/50 dark:text-zinc-300 dark:border-zinc-700 dark:hover:border-zinc-600"
                  )}
                >
                  <PlusIcon className="w-3.5 h-3.5" />
                  Create "{input}"
                </button>
              )}
            </div>
          </div>
        )}
      </div>

      {error && (
        <p className="text-sm text-red-600 dark:text-red-400">{error}</p>
      )}
    </div>
  );
}

export { TagInput };

Browse & preview free → Unlock everything — from $19/mo

More like this

Arceage Contact Us — Form website build-promptArceage Contact Us Dialog — Form website build-promptDialog AI Voice Input — Form website build-promptAI Voice Input AI Input With Loading — Form website build-promptAI Input With Loading AI Input With Search — Form website build-promptAI Input With Search Dotted Dialog — Form website build-promptDotted Dialog
Meez.

The biggest library of AI website build-prompts & motion backgrounds — at the lowest price.

Library
Browse all Prompts by category Section prompts Motion backgrounds Pricing
Learn & contact
How it works Why AI sites look generic Terms Privacy Contact Affiliates
© Meez — an open archive. Start free · Unlimited from $19/mo · cancel anytime.